Skip to content

Commit

Permalink
Update docs for v0.7.0: new Chorex runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
ashton314 committed Jan 22, 2025
1 parent 4ff60cc commit 9850d59
Show file tree
Hide file tree
Showing 3 changed files with 19 additions and 118 deletions.
121 changes: 15 additions & 106 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,16 +191,14 @@ Local functions are not defined as part of the choreography; instead, you implem
### `if` expressions and knowledge of choice broadcasting

```elixir
if Actor1.make_decision() do
Actor1[L] ~> Actor2
if Actor1.make_decision(), notify: [Actor2] do
...
else
Actor1[R] ~> Actor2
...
end
```

`if` expressions are supported. Some actor makes a choice of which branch to go down. It is then *crucial* (and, at this point, entirely up to the user) that that deciding actor inform all other actors about the choice of branch with the special `ActorName[L] ~> OtherActorName` syntax. Note the lack of `.` and variable names. Furthermore, the true branch is always `L` (left) and the false branch is always `R` (right).
`if` expressions are supported. Some actor makes a choice of which branch to go down. It is then *crucial* that that deciding actor inform all other actors about the choice of branch with the special `notify: [Actor2, Actor3, ...]` syntax. If this is omitted, *all* actors will be informed, which may lead to more messages being sent than necessary.

### Function syntax

Expand Down Expand Up @@ -335,12 +333,6 @@ receive do
end
```

## Shared-state choreographies

Sometimes you might have a choreography where one or more actors need to share some state between different instantiations of the choreography. Returning to our bookseller example, the bookseller might need to keep track of a finite stock of books and ensure that no book gets double-sold.

Chorex can let you share state between different instances of the bookseller actor through a proxy. Details are under the `Chorex` module.

## Using a choreography with the rest of your project

The local functions are free to call any other code you have—they're just normal Elixir. If that code sends and receives messages not managed by the choreography library, there is no guarantee that this will be deadlock-free.
Expand All @@ -356,6 +348,18 @@ If you find any bugs or would like to suggest a feature, please [open an issue o

We will collect change descriptions here until we come up with a more stable format when changes get bigger.

- v0.7.0, 2025-01-22

New runtime model.

- v0.6.0, 2025-01-09

Big rewrite to project actors to GenServers under the hood.

- v0.5.0, 2025-11-15

Protection against out-of-order messages with communication integrity tokens.

- v0.4.3; 2024-08-13

Multi-clause `with` blocks work.
Expand Down Expand Up @@ -400,103 +404,8 @@ The `defchor` macro is implemented in the `Chorex` module.
- This gathering is handled by the `WriterMonad` module, which provides the `monadic do ... end` form as well as `return` and `mzero`.
- Finally the macro generates modules for each actor under the `Chorex` module it generates.

So, for example, if you have a simple choreography like this:

```elixir
defchor [Alice, Bob] do
def run() do
Alice.pick_modulus() ~> Bob.(m)
Bob.gen_key(m) ~> Alice.(bob_key)
Alice.encrypt(message, bob_key)
end
end
```

This will get transformed into (roughly) this code:

```elixir
defmodule Chorex do
def get_actors() do
[Alice, Bob]
end

def alice do
quote do
import Alice
@behaviour Alice
def init(args) do
Alice.init(__MODULE__, args)
end
end
end

defmodule Alice do
@callback encrypt(any(), any()) :: any()
@callback pick_modulus() :: any()
import Chorex.Proxy, only: [send_proxied: 2]

def init(impl, args) do
receive do
{:config, config} ->
arg = Enum.at(args, 0, nil)
ret = run(impl, config, arg)
send(config[:super], {:chorex_return, Alice, ret})
end
end

def run(impl, config, _) do
send(config[Bob], impl.pick_modulus())

bob_key =
receive do
msg -> msg
end

impl.encrypt(message, bob_key)
end
end

def bob do
quote do
import Bob
@behaviour Bob
def init(args) do
Bob.init(__MODULE__, args)
end
end
end

defmodule Bob do
@callback gen_key(any()) :: any()
import Chorex.Proxy, only: [send_proxied: 2]

def init(impl, args) do
receive do
{:config, config} ->
arg = Enum.at(args, 0, nil)
ret = run(impl, config, arg)
send(config[:super], {:chorex_return, Bob, ret})
end
end

def run(impl, config, _) do
m =
receive do
msg -> msg
end

send(config[Alice], impl.gen_key(m))
end
end

defmacro __using__(which) do
apply(__MODULE__, which, [])
end
end
```

You can see there's a `Chorex.Alice` module and a `Chorex.Bob` module.

Each actor projects to GenServer. The GenServer maintains some state at runtime: most importantly, it tracks the function call stack and an inbox of pending Chorex messages.

## Testing

Expand Down
2 changes: 1 addition & 1 deletion internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Stack gets pushed when calling a function. (Search `# Application projection` in

Example of a return can be found wherever `make_continue` is called.

### Manual startup
### Manual startup

To start the choreography, you need to invoke the `init` function in
each of your actors (provided via the `use ...` invocation)
Expand Down
14 changes: 3 additions & 11 deletions using_chorex.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,11 @@ defmodule ThreePartySeller do
Seller.get_price("book:" <> b) ~> Buyer2.(p)
Buyer2.compute_contrib(p) ~> Buyer1.(contrib)

if Buyer1.(p - contrib < get_budget()) do
Buyer1[L] ~> Seller
if Buyer1.(p - contrib < get_budget()), notify: [Seller] do
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
Expand Down Expand Up @@ -150,13 +148,11 @@ defmodule TestChor3 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
if Buyer3.decision, notify: [Seller3] do
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
Expand All @@ -175,13 +171,9 @@ defmodule TestChor3 do
end

def run(Buyer3.(get_contribution?)) do
if Buyer3.(get_contribution?) do
Buyer3[L] ~> Contributor3
Buyer3[L] ~> Seller3
if Buyer3.(get_contribution?), notify: [Contributor3, Seller3] do
bookseller(@two_party/1)
else
Buyer3[R] ~> Contributor3
Buyer3[R] ~> Seller3
bookseller(@one_party/1)
end
end
Expand Down

0 comments on commit 9850d59

Please sign in to comment.