View Source Chorex (Chorex v0.1.0)

Make your modules dance!

Chorex allows you to specify a choreography: a birds-eye view of an interaction of concurrent parties. Chorex takes that choreography creates a projection of that interaction for each party in the system.

Take, for example, the classic problem of a book seller and two buyers who want to split the price. The interaction looks like this:

+------+         +------+ +------+
|Buyer1|         |Seller| |Buyer2|
+--+---+         +--+---+ +--+---+
   |                |        |
   |   Book title   |        |
   |--------------->|        |
   |                |        |
   |     Price      |        |
   |<---------------|        |
   |                |        |
   |                |  Price |
   |                |------->|
   |                |        |
   |      Contribution       |
   |<------------------------|
   |                |        |
   |   Buy/No buy   |        |
   |--------------->|        |
   |                |        |
   |(if Buy) address|        |
   |--------------->|        |
   |                |        |
   | Shipping date  |        |
   |<---------------|        |
+--+---+         +--+---+ +--+---+
|Buyer1|         |Seller| |Buyer2|
+------+         +------+ +------+

You can encode that interaction with the defchor macro and DSL:

defmodule ThreePartySeller do
  defchor [Buyer1, Buyer2, Seller] do
    Buyer1.get_book_title() ~> Seller.(b)
    Seller.get_price("book:" <> b) ~> Buyer1.(p)
    Seller.get_price("book:" <> b) ~> Buyer2.(p)
    Buyer2.compute_contrib(p) ~> Buyer1.(contrib)

    if Buyer1.(p - contrib < get_budget()) do
      Buyer1[L] ~> Seller
      Buyer1.get_address() ~> Seller.(addr)
      Seller.get_delivery_date(b, addr) ~> Buyer1.(d_date)
      Buyer1.(d_date)
    else
      Buyer1[R] ~> Seller
      Buyer1.(nil)
    end
  end
end

The defchor macro will take care of generating code that handles sending messages. Now all you have to do is implement the local functions that don't worry about the outside system:

defmodule Seller do
  use ThreePartySeller.Chorex, :seller

  def get_price(book_name), do: ...
  def get_delivery_date(book_name, addr), do: ...
end

defmodule Buyer1 do
  use ThreePartySeller.Chorex, :buyer1

  def get_book_title(), do: ...
  def get_address(), do: ...
  def get_budget(), do: ...
end

defmodule Buyer2 do
  use ThreePartySeller.Chorex, :buyer2

  def compute_contrib(price), do: ...
end

What the defchor macro actually does is creates a module Chorex and submodules for each of the actors: Chorex.Buyer1, Chorex.Buyer2 and Chorex.Seller. There's a handy __using__ macro that will Do the Right Thing™ when you say use Mod.Chorex, :actor_name and will import those modules and say that your module implements the associated behaviour. That way, you should get a nice compile-time warning if a function is missing.

To start the choreography, you need to invoke the init function in each of your actors (provided via the use ... invocation) whereupon each actor will wait to receive a config mapping actor name to PID:

the_seller = spawn(MySeller, :init, [])
the_buyer1 = spawn(MyBuyer1, :init, [])
the_buyer2 = spawn(MyBuyer2, :init, [])

config = %{Seller1 => the_seller, Buyer1 => the_buyer1, Buyer2 => the_buyer2, :super => self()}

send(the_seller, {:config, config})
send(the_buyer1, {:config, config})
send(the_buyer2, {:config, config})

assert_receive {:choreography_return, Buyer1, ~D[2024-05-13]}

Each of the parties will try sending the last value they computed once they're done running.

Higher-order choreographies

Chorex supports higher-order choreographies. For example, you can define a generic buyer/seller interaction and abstract away the decision process into a higher-order choreography:

defmodule TestChor3 do
  defchor [Buyer3, Contributor3, Seller3] do
    def bookseller(decision_func) do
      Buyer3.get_book_title() ~> Seller3.the_book
      with Buyer3.decision <- decision_func.(Seller3.get_price("book:" <> the_book)) do
        if Buyer3.decision do
          Buyer3[L] ~> Seller3
          Buyer3.get_address() ~> Seller3.the_address
          Seller3.get_delivery_date(the_book, the_address) ~> Buyer3.d_date
          Buyer3.d_date
        else
          Buyer3[R] ~> Seller3
          Buyer3.(nil)
        end
      end
    end

    def one_party(Seller3.(the_price)) do
      Seller3.(the_price) ~> Buyer3.(p)
      Buyer3.(p < get_budget())
    end

    def two_party(Seller3.(the_price)) do
      Seller3.(the_price) ~> Buyer3.(p)
      Seller3.(the_price) ~> Contributor3.(p)
      Contributor3.compute_contrib(p) ~> Buyer3.(contrib)
      Buyer3.(p - contrib < get_budget())
    end

    bookseller(&two_party/1)
  end
end

This will run the two-buyer scenario by default. If you want to cut the second buyer out of the picture, define a function called run_choreography for the buyer and seller actors and have them compose the one_party and bookseller functions.

defmodule MySeller31 do
  use TestChor3.Chorex, :seller3

  def get_delivery_date(_book, _addr) do
    ~D[2024-05-13]
  end

  def get_price("book:Das Glasperlenspiel"), do: 42
  def get_price("book:Zen and the Art of Motorcycle Maintenance"), do: 13

  def run_choreography(impl, config) do
    Seller3.bookseller(impl, config, &Seller3.one_party/3)
  end
end

defmodule MyBuyer31 do
  use TestChor3.Chorex, :buyer3

  def get_book_title(), do: "Zen and the Art of Motorcycle Maintenance"
  def get_address(), do: "Maple Street"
  def get_budget(), do: 22

  def run_choreography(impl, config) do
    Buyer3.bookseller(impl, config, &Buyer3.one_party/3)
  end
end

It's important to remember to pass impl and config around. These are internal to the workings of the Chorex module, so do not modify them.

Summary

Functions

Get the actor name from an expression

Define a new choreography.

Perform the control merge function, but flatten block expressions at each step

Perform endpoint projection in the context of node label.

Like project/3, but focus on handling ActorName.local_var, ActorName.local_func() or ActorName.(local_exp). Handles walking the local expression to gather list of functions needed for the behaviour to implement.

Functions

Link to this function

actor_from_local_exp(actor_alias, env)

View Source

Get the actor name from an expression

iex> Chorex.actorfromlocal_exp((quote do: Foo.bar(42)), __ENV)

Link to this macro

defchor(arglist, list)

View Source (macro)

Define a new choreography.

Link to this function

do_local_project_wrapper(code, acc, env, label)

View Source
Link to this macro

is_immediate(x)

View Source (macro)

Perform the control merge function, but flatten block expressions at each step

Link to this function

project(expr, env, label)

View Source
@spec project(term(), Macro.Env.t(), atom()) :: WriterMonad.t()

Perform endpoint projection in the context of node label.

This returns a pair of a projection for the label, and a list of behaviors that an implementer of the label must implement.

Link to this function

project_global_func(arg, body, env, label)

View Source
Link to this function

project_local_expr(arg, env, label)

View Source

Like project/3, but focus on handling ActorName.local_var, ActorName.local_func() or ActorName.(local_exp). Handles walking the local expression to gather list of functions needed for the behaviour to implement.

Link to this function

project_local_func(arg, body, env, label)

View Source
Link to this function

project_sequence(expr, env, label)

View Source
@spec project_sequence(term(), Macro.Env.t(), atom()) :: WriterMonad.t()
Link to this function

walk_local_expr(code, env, label)

View Source