{"id":26399197,"url":"https://github.com/utahplt/chorex","last_synced_at":"2025-03-17T13:18:57.565Z","repository":{"id":237097805,"uuid":"790989811","full_name":"utahplt/chorex","owner":"utahplt","description":"Choreographic programming in Elixir","archived":false,"fork":false,"pushed_at":"2024-10-29T23:09:59.000Z","size":185,"stargazers_count":20,"open_issues_count":8,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-10-30T01:48:12.978Z","etag":null,"topics":["choreographic-programming","elixir","metaprogramming"],"latest_commit_sha":null,"homepage":"https://hex.pm/packages/chorex","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/utahplt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-23T22:26:42.000Z","updated_at":"2024-10-21T17:45:43.000Z","dependencies_parsed_at":"2024-05-21T05:21:13.666Z","dependency_job_id":"c483355d-318f-4b99-8bbe-c256dd342a8d","html_url":"https://github.com/utahplt/chorex","commit_stats":null,"previous_names":["utahplt/chorex"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/utahplt%2Fchorex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/utahplt%2Fchorex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/utahplt%2Fchorex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/utahplt%2Fchorex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/utahplt","download_url":"https://codeload.github.com/utahplt/chorex/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244039231,"owners_count":20387835,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["choreographic-programming","elixir","metaprogramming"],"created_at":"2025-03-17T13:18:56.847Z","updated_at":"2025-03-17T13:18:57.553Z","avatar_url":"https://github.com/utahplt.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"Chorex - Choreographic Programming in Elixir\n\n[![Chorex Tests](https://github.com/utahplt/chorex/actions/workflows/elixir.yml/badge.svg)](https://github.com/utahplt/chorex/actions/workflows/elixir.yml)\n\n# Synopsis\n\n**Note:** this documentation is current as of 2025-02-24. The project is evolving rapidly, so this README may occasionally get out-of-sync with what the project can do.\n\nAdd `Chorex.Registry` to your application setup:\n\n```elixir\n# part of application startup; e.g. in a Phoenix application this\n# would be in MyApp.Application located at lib/my_app/application.ex\nchildren = [\n  {Registry, name: Chorex.Registry, keys: :unique}\n]\n```\n\nDescribe the choreography in a module with the `defchor` macro:\n\n```elixir\ndefmodule TestChor do\n  defchor [Buyer, Seller] do\n    def run(Buyer.(book_title)) do\n      Buyer.(book_title) ~\u003e Seller.(b)\n      Seller.get_price(b) ~\u003e Buyer.(p)\n      Buyer.(p)\n    end\n  end\nend\n```\n\nImplement the actors:\n\n```elixir\ndefmodule MyBuyer do\n  use TestChor.Chorex, :buyer\nend\n\ndefmodule MySeller do\n  use TestChor.Chorex, :seller\n\n  def get_price(\"Das Glasperlenspiel\"), do: 42\n  def get_price(\"A Tale of Two Cities\"), do: 16\nend\n```\n\nElsewhere in your program:\n\n```elixir\nChorex.start(TestChor.Chorex, %{Seller =\u003e MySeller, Buyer =\u003e MyBuyer}, [\"Das Glasperlenspiel\"])\n\nreceive do\n  {:chorex_return, Buyer, val} -\u003e\n    IO.puts(\"Got #{val}\")            # prints \"Got 42\"\nend\n\nChorex.start(TestChor.Chorex, %{Seller =\u003e MySeller, Buyer =\u003e MyBuyer}, [\"A Tale of Two Cities\"])\n\nreceive do\n  {:chorex_return, Buyer, val} -\u003e\n    IO.puts(\"Got #{val}\")            # prints \"Got 16\"\nend\n```\n\n# Description\n\nChorex is a library for *choreographic programming* in Elixir. Choreographic programming is a programming paradigm where you specify the interactions between different entities in a concurrent system in one global view, and then *extract implementations* for each of those actors. See [§ Bibliography](#org44e5aee) for references on choreographic programming in general.\n\n\n## Installation\n\nChorex is available on Hex.pm. Install by including the following in your `mix.exs` file under the `deps` list:\n\n```elixir\ndef deps do\n  [\n    ...,\n    {:chorex, \"~\u003e 0.8.0\"},\n    ...\n  ]\nend\n```\n\nYou can install development versions of Chorex directly from GitHub like so:\n\n```elixir\ndef deps do\n  [\n    ...,\n    {:chorex, github: \"utahplt/chorex\"},\n    ...\n  ]\nend\n```\n\nAdd `Chorex.Registry` to your application setup:\n\n```elixir\n# part of application startup; e.g. in a Phoenix application this\n# would be in MyApp.Application located at lib/my_app/application.ex\nchildren = [\n  {Registry, name: Chorex.Registry, keys: :unique}\n]\n```\n\nNote that this is *experimental software* and stuff *will* break. Please don't rely on this for anything production-grade. Not yet at least.\n\n\n## What is a choreography?\n\nA 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.\n\nLindsey Kuper's research group has put together [a delightful zine explaining choreographic programming](https://decomposition.al/zines/#communicating-chorrectly-with-a-choreography). Check that out if you are new to choreographies.\n\nAt a high-level, Chorex lets you build choreographies to describe different interactions between components of your system. Chorex focuses on the communication flow; you still implement the computation that runs locally on each node, but you don't have to worry about writing `send`s between nodes.\n\n\u003c!-- FIXME: link the examples repo here --\u003e\nOnce you have a choreography, you can *instantiate* it any number of times as you like. You might want, for example, to have one choreography describing how a user actor would communicate to create an account on a system, and then another choreography for how an existing user would log in with previously established credentials.\n\n\n## Choreography syntax\n\nChorex introduces some new Elixir syntax for choreographies. Here's a breakdown of how it works.\n\nStart by creating a module to hold the choreography, say `import Chorex`, and add a `defchor` block:\n\n```elixir\ndefmodule MyCoolChoreography do\n  import Chorex\n\n  defchor [Actor1, Actor2, ...] do\n    ...choreography body...\n  end\nend\n```\n\n(Note: in addition to the choreography definition here, you will need to make a module for each actor. We'll focus on the special syntax of the `defchor` block in this section, but later you'll see how to built a module for each of the `Actor1`, `Actor2`, etc.)\n\nThe `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.\n\nThe body of the choreography is a set of functions. One function named `run` must be present: this serves as the entry point into the choreography. The arguments to `run` come from the third argument to the `Chorex.start` function and are how you typically get values into an instantiation of a choreography. (More on `Chorex.start` and function parameters in a minute.)\n\n```elixir\ndefchor [Actor1, Actor2, ...] do\n  def some_func(...) do\n    ...\n  end\n\n  def run() do\n    ...\n  end\nend\n```\n\n\n### Message passing expressions\n\nInside the body of functions you can write message passing expressions. Examples:\n\n```elixir\nActor1.(var1) ~\u003e Actor2.(var2_a)\nActor1.func_1() ~\u003e Actor2.(var2_b)\nActor1.func_2(var1_a, var1_b) ~\u003e Actor2.(var2_c)\nActor1.(var1_a + var1_b) ~\u003e Actor2.(var2_c)\n```\n\nFormal syntax:\n\n```bnf\n  message_pass ::= $local_exp ~\u003e $actor.($pat)\n\n  local_exp    ::= $actor.($pat)\n                 | $actor.$func($exp, ...)\n                 | $actor.($exp)\n\n  actor        ::= Module name         (e.g. Actor)\n  func         ::= Function name       (e.g. frobnicate(...))\n  pat          ::= Pattern match expr  (e.g. a variable like `foo` or tuples `{:ok, bar}` etc.)\n  exp          ::= Elixir expression   (e.g. foo + sum([1, 2, 3]))\n```\n\nThe `~\u003e` indicates sending a message between actors. The left-hand-side must be `Actor1.\u003csomething\u003e`, where that `\u003csomething\u003e` bit can be one of three things:\n\n1.  A variable local to Actor1\n2.  A function local to Actor1 (with or without arguments, also all local to Actor1)\n3.  An expression local to Actor1\n\nThe right-and-side must be `Actor2.(\u003cpattern\u003e)`. 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`.\n\n\n### Local expressions\n\n*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:\n\n```elixir\ndefchor [Holmes, Watson] do\n  def discombobulate(Holmes.(clue)) do\n    ...\n  end\nend\n```\n\nThen inside the body of that function, I can talk about the variable `clue` which is located on the `Holmes` node. I can't, for instance, talk about the variable `clue` on the `Watson` node.\n\n```elixir\nHolmes.(clue + 1)    # fine\nWatson.(clue * 2)    # error: variable `clue` not defined\n```\n\nI can *send* the value in Holmes' `clue` variable to Watson, at which point Watson can do computation with the value:\n\n```elixir\nHolmes.(clue) ~\u003e Watson.(holmes_observes)\n\nif Watson.remember(holmes_observes) do\n  ...\nelse\n  ...\nend\n```\n\nThe `remember` function here will be defined on the the implementation for the `Watson` actor.\n\n**ACHTUNG!! `mix format` will rewrite `Actor1.var1` to `Actor1.var1()` which is a function call instead of a variable! Wrap variables in parens like `Actor1.(var1)` if you want to use `mix format`!** This is an unfortunate drawback—suggestions on fixing this would be welcome.\n\nLocal functions are not defined as part of the choreography; instead, you implement these in a separate Elixir module. More on that later.\n\n\n### `if` expressions and knowledge of choice broadcasting\n\n```elixir\nif Actor1.make_decision(), notify: [Actor2] do\n  ...\nelse\n  ...\nend\n```\n\n`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.\n\n\n### Function syntax\n\n```elixir\ndefchor [Alice, Bob] do\n  def run(Alice.(msg)) do\n    with Bob.({pub, priv}) \u003c- Bob.gen_key() do\n      Bob.(pub) ~\u003e Alice.(key)\n      exchange_message(Alice.encrypt(msg \u003c\u003e \"\\n  love, Alice\", key), Bob.(priv))\n    end\n  end\n\n  def exchange_message(Alice.(enc_msg), Bob.(priv)) do\n    Alice.(enc_msg) ~\u003e Bob.(enc_msg)\n    Alice.(:letter_sent)\n    Bob.decrypt(enc_msg, priv)\n  end\nend\n```\n\nChoreographies 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:\n\n```elixir\nexchange_message(Bob.(msg), Alice.(priv))\n```\n\n(and not just because the variables are wrong—the actor names don't match so the parameters won't get the values they need).\n\n\n### Higher-order choreographies\n\n```elixir\ndef higher_order_chor(other_chor) do\n  ... other_chor.(...) ...\nend\n```\n\nChorex 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:\n\n```elixir\ndefchor [Actor, OtherActor] do\n  def higher_order_chor(other_chor) do\n    ... other_chor.(...) ...\n  end\n\n  def some_local_chor(Actor.(var_name)) do\n    Actor.(var_name) ~\u003e OtherActor.(other_var)\n    OtherActor.(other_var)\n  end\n\n  def run() do\n    higher_order_chor(@some_local_chor/1)\n  end\nend\n```\n\nNote 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.\n\n\n### Variable binding\n\n```elixir\nwith OtherActor.(other_var) \u003c- other_chor.(Actor.(var)) do\n  ...\nend\n```\n\nYou 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.\n\n\n### Error recovery\n\n```elixir\ndefmodule Bookstore do\n  import Chorex\n\n  defchor [Buyer, Seller, Contributor] do\n    def run() do\n      Buyer.get_book_title() ~\u003e Seller.(book)\n      Seller.get_price(book) ~\u003e Buyer.(price)\n      Seller.get_price(book) ~\u003e Contributor.(price)\n\n      try do\n        Contributor.compute_contribution(price) ~\u003e Buyer.(extra_money) # might blow up\n\n        if Buyer.in_budget(price - extra_money) do\n          ...\n        else\n          ...\n        end\n      rescue\n        if Buyer.in_budget(price) do # no extra money!\n          Buyer.(\"thanks anyway\") ~\u003e Contributor.(thank_you_note)\n          ...\n        else\n          ...\n        end\n      end\n    end\n  end\nend\n```\n\nChorex supports exceptions in the form of actors crashing. In the above example, suppose the function `compute_contribution` is known to possibly crash at runtime. In accordance with the Erlang/Elixir philosophy of \"let it crash\", suppose we would rather recover from this crash than harden the `Contributor` actor to prevent crashes.\n\nIn the case of a crash inside the `try` block, the crasher will get restarted, and all actors will abort execution of the `try` block and move to the `rescue` block.\n\n\n## Creating a choreography: `defchor` + actor implementations\n\nTo create a choreography, start by making a module, and writing the choreography with the `defchor` macro.\n\n```elixir\ndefmodule Bookstore do\n  import Chorex\n\n  defchor [Actor1, Actor2] do\n    def run() do\n      Actor1.(... some expr ...) ~\u003e Actor2.(some_var)\n      Actor2.some_computation(some_var) ~\u003e Actor1.(the_result)\n      ...\n    end\n  end\nend\n```\n\nYou will need to make a module for every actor you specify at the beginning of `defchor` and mark which actor you're implementing like so:\n\n```elixir\ndefmodule MyFirstActor do\n  use Bookstore.Chorex, :actor1\n\n  ...\nend\n\ndefmodule MySecondActor do\n  use Bookstore.Chorex, :actor2\n\n  def some_computation(val), do: ...\nend\n```\n\nThese modules will need to implement all of the local functions specified in the choreography. Chorex will use Elixir's behaviour mechanism to warn you if you don't implement every function needed. In the above example, the `MySecondActor` implements the role of `Actor2` in the choreography, and therefore needs to implement the `some_computation` function.\n\n**Note:** *Actor names do not need to be the same as the modules implementing them!* It is *useful* to do that, but there exist instances where you might want to write one choreography and implement it in different ways.\n\n\n## Running a choreography\n\nYou need three things to fire off a choreography:\n\n1. The choreography description\n2. An implementation for each of the actors\n3. A call to `Chorex.start`\n\nUse the `Chorex.start/3` function to start a choreography:\n\n```elixir\nChorex.start(MyChoreography.Chorex,\n             %{ Actor1 =\u003e MyActor1Impl,\n                Actor2 =\u003e MyActor2Impl },\n             [args, to, run])\n```\n\nThe arguments are as follows:\n\n 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.)\n 2. A map from actor name to implementation module name.\n 3. A list of arguments to the `run` function in the Choreography. These will automatically get sent to the right nodes.\n\nOnce 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:\n\n```elixir\nreceive do\n  {:chorex_return, Actor1, val} -\u003e IO.inspect(val, label: \"Actor1's return: \")\nend\n```\n\n\n## Using a choreography with the rest of your project\n\nThe 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.\n\n\n# Development\n\nChorex is under active development and things will change and break rapidly.\n\nIf you find any bugs or would like to suggest a feature, please [open an issue on GitHub](https://github.com/utahplt/chorex/issues).\n\n## Changelog\n\nWe will collect change descriptions here until we come up with a more stable format when changes get bigger.\n\n - v0.8.2, 2025-02-28\n \n   Bug fix with some function parameters not making it into the context before `try/rescue` checkpoint.\n\n - v0.8.1, 2025-02-24\n \n   `with` blocks can be in non-tail position. Compile error on missing branch broadcast.\n\n - v0.8.0, 2025-02-10\n \n   Error recovery. 🎉 First-ever in a choreographic system! 🎉\n\n - v0.7.0, 2025-01-22\n \n   New runtime model.\n\n - v0.6.0, 2025-01-09\n \n   Big rewrite to project actors to GenServers under the hood.\n\n - v0.5.0, 2025-11-15\n \n   Protection against out-of-order messages with communication integrity tokens.\n\n - v0.4.3; 2024-08-13\n \n   Multi-clause `with` blocks work.\n\n - v0.4.2; 2024-08-07\n \n   Bugfix: projecting local expressions that call out to an Erlang module.\n\n - v0.4.1; 2024-08-01\n\n   Bugfix: choreographies can now have literal maps in local expressions.\n\n - v0.4.0; 2024-08-01\n \n   Functions can take arbitrary number of arguments from different actors.\n\n - v0.3.1; 2024-07-30\n\n   Fix many problems around local expression projection.\n\n - v0.3.0; 2024-07-22\n\n   Add `Chorex.start` and `run` function as an entry-point into the choreography.\n\n - v0.2.0; 2024-07-03\n\n   Add shared-state actors.\n\n - v0.1.0; 2024-05-30\n\n   Initial release. Lots of rough edges so please, be patient. :)\n\n\n## High-level internals\n\nThe `defchor` macro is implemented in the `Chorex` module.\n\n-   The `defchor` macro gathers a list of actors.\n-   For each actor, call `project` on the body of the choreography. The `project` function keeps track of the current actor as the `label` variable. (This vernacular borrowed from the academic literature.)\n-   The functions `project` and `project_sequence` are mutually recursive: `project_sequence` gets invoked whenever `project` encounters a block with multiple instructions.\n-   The `project` function walks the AST, it gathers a list of functions that will need to be implemented by each actor's implementing module, as well as a list of top-level functions for each projection.\n    -   This gathering is handled by the `WriterMonad` module, which provides the `monadic do ... end` form as well as `return` and `mzero`.\n-   Finally the macro generates modules for each actor under the `Chorex` module it generates.\n\n\nEach 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.\n\n\n## Testing\n\nSimply clone the repository and run `mix test`.\n\n\n\u003ca id=\"org44e5aee\"\u003e\u003c/a\u003e\n\n# Bibliography\n\n-   Hirsch \u0026 Garg (2022-01-16) *Pirouette: Higher-Order Typed Functional Choreographies*, Proceedings of the ACM on Programming Languages. \u003chttps://doi.org/10.1145/3498684\u003e\n\n-   Lugović \u0026 Montesi (2023-10-15) *Real-World Choreographic Programming: Full-Duplex Asynchrony and Interoperability*, The Art, Science, and Engineering of Programming. \u003chttps://doi.org/10.22152/programming-journal.org/2024/8/8\u003e\n\n\n# Authors\n\nThis is a project by the [Utah PLT](https://github.com/utahplt) group. Primary development by [Ashton Wiersdorf](https://lambdaland.org).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Futahplt%2Fchorex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Futahplt%2Fchorex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Futahplt%2Fchorex/lists"}