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
Get the actor name from an expression
iex> Chorex.actorfromlocal_exp((quote do: Foo.bar(42)), __ENV)
Define a new choreography.
Perform the control merge function, but flatten block expressions at each step
@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.
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.
@spec project_sequence(term(), Macro.Env.t(), atom()) :: WriterMonad.t()