Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functions now take an arbitrary number of arguments #16

Merged
merged 10 commits into from
Aug 1, 2024
82 changes: 57 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Chorex is available on Hex.pm. Install by including the following in your `mix.e
def deps do
[
...,
{:chorex, "~> 0.1.0"},
{:chorex, "~> 0.4.0"},
...
]
end
Expand All @@ -91,7 +91,7 @@ Note that this is *experimental software* and stuff *will* break. Please don't r

A choreography is a birds-eye view of an interaction between nodes in a distributed system. You have some set of *actors*—in Elixir parlance processes—that exchange *messages* while also running some *local computation*—i.e. functions that don't rely on talking to other nodes in the system.

### Choreography syntax
## Choreography syntax

Chorex introduces some new syntax for choreographies. Here's a breakdown of how it works:

Expand All @@ -103,20 +103,22 @@ end

The `defchor` macro wraps a choreography and translates it into core Elixir code. You give `defchor` a list of actors, specified as if they were module names, and then a `do` block wraps the choreography body.

The body of the choreography is a set of functions. One function named `run` must be present; this will serve as the entry point into the choreography. `run` takes one argument, which may be `_` if you don't need it. This argument comes from when you instantiate the choreography with `Chorex.start`. (More on `Chorex.start` in a minute.)
The body of the choreography is a set of functions. One function named `run` must be present; this will serve as the entry point into the choreography. The arguments to `run` come from the third argument to the `Chorex.start` function. (More on `Chorex.start` and function parameters in a minute.)

```elixir
defchor [Actor1, Actor2, ...] do
def some_func(...) do
...
end

def run(_) do
def run() do
...
end
end
```

### Message passing expressions

Inside the body of functions you can write message passing expressions. Examples:

```elixir
Expand All @@ -129,15 +131,15 @@ Actor1.(var1_a + var1_b) ~> Actor2.(var2_c)
Formal syntax:

```bnf
message_pass ::= $local_exp ~> $actor.($var)
message_pass ::= $local_exp ~> $actor.($pat)

local_exp ::= $actor.($var)
local_exp ::= $actor.($pat)
| $actor.$func($exp, ...)
| $actor.($exp)

actor ::= Module name (e.g. Actor)
func ::= Function name (e.g. frobnicate(...))
var ::= Variable name (e.g. foo, i)
pat ::= Pattern match expr (e.g. a variable like `foo` or tuples `{:ok, bar}` etc.)
exp ::= Elixir expression (e.g. foo + sum([1, 2, 3]))
```

Expand All @@ -147,7 +149,9 @@ The `~>` indicates sending a message between actors. The left-hand-side must be
2. A function local to Actor1 (with or without arguments, also all local to Actor1)
3. An expression local to Actor1

The right-and-side must be `Actor2.(<var_name>)`. This means that the left-hand-side will be computed on `Actor1` and send to `Actor2` where it will be stored in variable `<var_name>`.
The right-and-side must be `Actor2.(<pattern>)`. This means that the left-hand-side will be computed on `Actor1` and send to `Actor2` where it will be matched against the pattern `pattern`.

### Local expressions

*Local expressions* are computations that happen on a single node. These computations are isolated from each other—i.e. every location has its own variables. For example, if I say:

Expand Down Expand Up @@ -184,6 +188,8 @@ The `remember` function here will be defined on the the implementation for the `

Local functions are not defined as part of the choreography; instead, you implement these in a separate Elixir module. More on that later.

### `if` expressions and knowledge of choice broadcasting

```elixir
if Actor1.make_decision() do
Actor1[L] ~> Actor2
Expand All @@ -196,24 +202,42 @@ 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).

### Function syntax

```elixir
def higher_order_chor(other_chor) do
... other_chor.(...) ...
defchor [Alice, Bob] do
def run(Alice.(msg)) do
with Bob.({pub, priv}) <- Bob.gen_key() do
Bob.(pub) ~> Alice.(key)
exchange_message(Alice.encrypt(msg <> "\n love, Alice", key), Bob.(priv))
end
end

def exchange_message(Alice.(enc_msg), Bob.(priv)) do
Alice.(enc_msg) ~> Bob.(enc_msg)
Alice.(:letter_sent)
Bob.decrypt(enc_msg, priv)
end
end
```

Chorex supports higher-order choreographies. These are choreographies that take another choreography as an argument where it can be applied like a function.
Choreographies support functions and function calls—even recursive ones. Function parameters need to be annotated with the actor they live at, and the arguments when calling the function need to match. Calling a function with the wrong actor will result in the parameter getting `nil`. E.g. calling `exchange_message` above like so will not work properly:

```elixir
def some_local_chor(Actor.(var_name)) do
Actor.(var_name) ~> OtherActor.(other_var)
OtherActor.(other_var)
end
exchange_message(Bob.(msg), Alice.(priv))
```

This creates a choreography that can be passed as an argument to the `higher_order_chor` function. This takes as an argument a variable living at a particular actor, and returns another value on a potentially different node.
(and not just because the variables are wrong—the actor names don't match so the parameters won't get the values they need).

You would combine the choreographies like so:
### Higher-order choreographies

```elixir
def higher_order_chor(other_chor) do
... other_chor.(...) ...
end
```

Chorex supports higher-order choreographies. This means you can pass the functions defined *inside the `defchor` block* around as you would with functions. Higher-order choreographic functions *don't* get an actor prefix and you call them as you would a function bound to a variable, like so:

```elixir
defchor [Actor, OtherActor] do
Expand All @@ -226,21 +250,25 @@ defchor [Actor, OtherActor] do
OtherActor.(other_var)
end

def run(_) do
higher_order_chor(&some_local_chor/1)
def run() do
higher_order_chor(@some_local_chor/1)
end
end
```

Right now these functions are limited to a single argument.
Note that when referring to the function, you **must** use the `@func_name/3` syntax—the Chorex compiler notices the `@` and processes the function reference differently. This is because the functions defined with `def` inside the `defchor` block have private internal details (when Chorex builds them, they get special implicit arguments added) and Chorex needs to handle references to these functions specially.

### Variable binding

```elixir
with OtherActor.(other_var) <- other_chor.(Actor.(var)) do
...
end
```

You can use `with` to bind a variable to the result of calling a higher-order choreography. Note that right now you can only have one `<-` in the expression.
You can bind the result of some expression to a variable/pattern at an actor with `with`. In the case of a higher-order choreography (seen above) this is whatever was on node `OtherActor` when `other_chor` executed. You may also use `with` for binding local expressions, as seen in the `exchange_message` example under § Function syntax.

Right now you can only have one `<-` in the expression and there can only be one actor that binds variables.


## Creating a choreography
Expand All @@ -250,7 +278,7 @@ To create a choreography, start by making a module, and writing the choreography
```elixir
defmodule Bookstore do
defchor [Actor1, Actor2] do
def run(_) do
def run() do
Actor1.(... some expr ...) ~> Actor2.(some_var)
Actor2.some_computation(some_var) ~> Actor1.(the_result)
...
Expand Down Expand Up @@ -293,14 +321,14 @@ Use the `Chorex.start/3` function to start a choreography:
Chorex.start(MyChoreography.Chorex,
%{ Actor1 => MyActor1Impl,
Actor2 => MyActor2Impl },
[arg_to_run])
[args, to, run])
```

The arguments are as follows:

1. The name of the `Chorex` module to use. (The `defchor` macro creates this module for you; in the above example there is a `MyChoreography` module with a top-level `defchor` declaration that creates the `Chorex` submodule on expansion.)
2. A map from actor name to implementation module name.
3. A list of arguments to the `run` function in the Choreography. (Right now, this only allows a single argument.)
3. A list of arguments to the `run` function in the Choreography. These will automatically get sent to the right nodes.

Once the actors are done, they will send the last value they computed to the current process tagged with the actor they were implementing. So, for this example, you could see what `Actor1` computed by awaiting:

Expand Down Expand Up @@ -331,6 +359,10 @@ 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.4.0; 2024-08-01

Functions can take arbitrary number of arguments from different actors.

- v0.3.1; 2024-07-30

Fix many problems around local expression projection.
Expand Down Expand Up @@ -363,7 +395,7 @@ So, for example, if you have a simple choreography like this:

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