Introduction

ChoRus Logo

Welcome to ChoRus! This website is a guide to using the ChoRus library.

What is ChoRus?

ChoRus is a library that enables Choreographic Programming in Rust.

In distributed programming, it is necessary to coordinate the behavior of multiple nodes. This coordination is typically achieved by writing a program that runs on each node. However, writing a program for each node can lead to bugs and inconsistencies.

Choreographic Programming is a programming paradigm that allows programmers to write "choreographies" that describe the desired behavior of a system as a whole. These choreographies can then be used to generate programs for each node in the system though a process called "end-point projection," or "EPP" for short.

Choreographic Programming as a Library

In the past, choreographic programming has been implemented as a standalone programming language. While this approach allows for flexibility for language designs, it makes it difficult to integrate choreographic programming into existing ecosystems.

ChoRus takes a different approach. Instead of implementing choreographic programming as a language, ChoRus implements choreographic programming as a library. ChoRus can be installed as a Cargo package and used in any Rust project. This allows choreographic programming to be used in any Rust project, including projects that use other libraries.

ChoRus is built on top of the "End-point Projection as Dependency Injection" (EPP-as-DI) approach. ChoRus also takes advantage of Rust's type system and macro system to provide a safe and ergonomic choreographic programming experience.

Features

At high level, ChoRus provides the following features:

  • Define choreographies by implementing the Choreography trait.
    • Passing located arguments to / receiving located return values from choreographies.
    • Location polymorphism.
    • Higher-order choreographies.
    • Efficient conditional with choreographic enclaves.
  • Performing end-point projection.
  • Pluggable message transports.
    • Two built-in transports: Local and HTTP.
    • Creating custom transports.
  • Macros for defining locations and choreographies.

Getting Started

Installation

ChoRus is still under active development. We are still expecting to make breaking changes to the API.

# create a binary crate
cargo new chorus_hello_world
cd chorus_hello_world
# install ChoRus as a dependency
cargo add chorus_lib

Running Hello World example

Once you have installed ChoRus, you can run the Hello World example by copy-pasting the following code into main.rs:

extern crate chorus_lib;

use std::thread;

use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, LocationSet, Projector};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};

// --- Define two locations (Alice and Bob) ---

#[derive(ChoreographyLocation)]
struct Alice;

#[derive(ChoreographyLocation)]
struct Bob;

// --- Define a choreography ---
struct HelloWorldChoreography;

// Implement the `Choreography` trait for `HelloWorldChoreography`
impl Choreography for HelloWorldChoreography {
    // Define the set of locations involved in the choreography.
    // In this case, the set consists of `Alice` and `Bob` and
    // the choreography can use theses locations.
    type L = LocationSet!(Alice, Bob);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        // Create a located value at Alice
        let msg_at_alice = op.locally(Alice, |_| {
            println!("Hello from Alice!");
            "Hello from Alice!".to_string()
        });
        // Send the located value to Bob
        let msg_at_bob = op.comm(Alice, Bob, &msg_at_alice);
        // Print the received message at Bob
        op.locally(Bob, |un| {
            let msg = un.unwrap(&msg_at_bob);
            println!("Bob received a message: {}", msg);
            msg
        });
    }
}

fn main() {
    let mut handles: Vec<thread::JoinHandle<()>> = Vec::new();
    // Create a transport channel
    let transport_channel = LocalTransportChannelBuilder::new()
        .with(Alice)
        .with(Bob)
        .build();
    // Run the choreography in two threads
    {
        let transport = LocalTransport::new(Alice, transport_channel.clone());
        handles.push(thread::spawn(move || {
            let p = Projector::new(Alice, transport);
            p.epp_and_run(HelloWorldChoreography);
        }));
    }
    {
        let transport = LocalTransport::new(Bob, transport_channel.clone());
        handles.push(thread::spawn(move || {
            let p = Projector::new(Bob, transport);
            p.epp_and_run(HelloWorldChoreography);
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}

Guide

This section will guide you through the concepts and features of ChoRus.

Locations

Before we can start writing choreographies, we need to define locations. A location is a place where a choreography can be executed. A location can be a physical location, such as a computer, or a logical location, such as a thread.

To define a location, we need to create a struct and derive the ChoreographyLocation trait.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::ChoreographyLocation;

#[derive(ChoreographyLocation)]
struct Alice;

#[derive(ChoreographyLocation)]
struct Bob;
}

The ChoreographyLocation trait provides the name method, which returns the name of the location as a &'static str. The name of a location is used to identify the location when performing end-point projection.

use chorus_lib::core::ChoreographyLocation;

#[derive(ChoreographyLocation)]
struct Alice;

#[derive(ChoreographyLocation)]
struct Bob;

let name = Alice::name();
assert_eq!(name, "Alice");

Location Set

A LocationSet is a special type representing a set of ChoreographyLocation types. It's used to ensure type safety within the system, and you'll see its application in future sections. To build a LocationSet type, you can use the LocationSet macro from the chorus_lib crate.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::ChoreographyLocation;
#[derive(ChoreographyLocation)]
struct Alice;

#[derive(ChoreographyLocation)]
struct Bob;
use chorus_lib::core::LocationSet;

type L = LocationSet!(Alice, Bob);
}

Choreography

Choreography is a program that describes the behavior of a distributed system as a whole. To define a choreography, create a struct and implement the Choreography trait.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
// 1. Define a struct
struct HelloWorldChoreography;

// 2. Implement the `Choreography` trait
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        // 3. Use the `op` parameter to access operators
        op.locally(Alice, |_| {
            println!("Hello, World!");
        });
    }
}
}

Choreography must implement the run method which defines the behavior of the system. The run method takes a reference to an object that implements the ChoreoOp trait. The ChoreoOp trait provides choreographic operators such as locally and comm.

Also, each Choreography has an associated LocationSet type, L; this is the LocationSet that the Choreography can operate on.

Choreographic Operators

Inside the run method, you can use the op parameter to access choreographic operators.

locally

The locally operator is used to perform a computation at a single location. It takes two parameters: a location and a closure. The closure is executed only at the specified location.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
op.locally(Alice, |_| {
    println!("Hello, World!");
});
    }
}
}

The closure can return a value to create a located value. Located values are values that are only available at a single location. When the computation closure returns a located value, the locally operator returns a located value at the same location.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
// This value is only available at Alice
let num_at_alice: Located<i32, Alice> = op.locally(Alice, |_| {
    42
});
    }
}
}

The computation closure takes Unwrapper. Using the Unwrapper, you can get a reference out of a located value.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
let num_at_alice: Located<i32, Alice> = op.locally(Alice, |_| {
    42
});
op.locally(Alice, |un| {
    let num: &i32 = un.unwrap(&num_at_alice);
    println!("The number at Alice is {}", num);
    assert_eq!(*num, 42);
});
    }
}
}

Note that you can unwrap a located value only at the location where the located value is available. If you try to unwrap a located value at a different location, the program will fail to compile.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice, Bob);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
// This code will fail to compile
let num_at_alice = op.locally(Alice, |_| { 42 });
op.locally(Bob, |un| {
    // Only values located at Bob can be unwrapped here
    let num_at_alice: &i32 = un.unwrap(&num_at_alice);
});
    }
}
}

We will discuss located values in more detail in the Located Values section.

comm

The comm operator is used to perform a communication between two locations. It takes three parameters: a source location, a destination location, and a located value at the source location. The located value is sent from the source location to the destination location, and the operator returns a located value at the destination location.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice, Bob);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
// This value is only available at Alice
let num_at_alice: Located<i32, Alice> = op.locally(Alice, |_| {
    42
});
// Send the value from Alice to Bob
let num_at_bob: Located<i32, Bob> = op.comm(Alice, Bob, &num_at_alice);
// Bob can now access the value
op.locally(Bob, |un| {
    let num_at_bob: &i32 = un.unwrap(&num_at_bob);
    println!("The number at Bob is {}", num_at_bob);
});
    }
}
}

broadcast

The broadcast operator is used to perform a broadcast from a single location to multiple locations. It takes two parameters: a source location and a located value at the source location. The located value is sent from the source location to all other locations, and the operator returns a normal value.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
// This value is only available at Alice
let num_at_alice: Located<i32, Alice> = op.locally(Alice, |_| {
    42
});
// Broadcast the value from Alice to all other locations
let num: i32 = op.broadcast(Alice, num_at_alice);
    }
}
}

Because all locations receive the value, the return type of the broadcast operator is a normal value, not a located value. This means that the value can be used for control flow.

if num == 42 {
    println!("The number is 42!");
} else {
    println!("The number is not 42!");
}

Note on invalid values for Choreography::L

You'll get a compile error if you try to work with a ChoreographyLocation that is not a member of L.

#![allow(unused)]
fn main() {
# extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
// 1. Define a struct
struct HelloWorldChoreography;

// 2. Implement the `Choreography` trait
// ...
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        // this will fail
        op.locally(Bob, |_| {
            println!("Hello, World!");
        });
    }
}
}

Located Values

As we have seen in the Choreography section, a located value is a value that is available only at a specific location. In this section, we will discuss located values in more detail.

Located struct

The Located struct represents a located value. It is a generic struct that takes two type parameters: a type parameter V that represents the type of the value, and a type parameter L1 that represents the location where the value is available.

pub struct Located<V, L1>
where
    L1: ChoreographyLocation,
{
    // ...
}

The Located struct can be in one of the two states: Local and Remote. The Local state represents a located value that is available at the current location. The Remote state represents a located value that is available at a different location.

Portable trait

Located values can be sent from one location to another using the comm operator and unwrapped using the broadcast operator if the value type implements the Portable trait.

trait Portable: Serialize + DeserializeOwned {}

The Portable is defined as above. The Serialize and DeserializeOwned traits are from the serde crate and are used to serialize and deserialize the value for communication.

The chorus_lib crate re-exports the Serialize and Deserialize from serde. In many cases, those traits can automatically be derived using the #[derive(Serialize, Deserialize)] attribute.

For the complete list of types that supports automatic derivation of Serialize and Deserialize, see the serde documentation. The documentation also explains how to implement Serialize and Deserialize for custom types.

Transport

In order to execute choreographies, we need to be able to send messages between locations. ChoRus provides a trait Transport that abstracts over message transports.

Built-in Transports

ChoRus provides two built-in transports: local and http.

The Local Transport

The local transport is used to execute choreographies on the same machine on different threads. This is useful for testing and prototyping.

To use the local transport, we first need to create a LocalTransportChannel, which works as a channel between threads and allows them to send messages to each other. To do so, we use the LocalTransportChannelBuilder struct from the chorus_lib crate.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreographyLocation, LocationSet};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
use chorus_lib::transport::local::LocalTransportChannelBuilder;

let transport_channel = LocalTransportChannelBuilder::new()
    .with(Alice)
    .with(Bob)
    .build();
}

Using the with method, we add locations to the channel. When we call build, it will create an instance of LocalTransportChannel.

Then, create a transport by using the LocalTransport::new function, which takes a target location (explained in the Projector section) and the LocalTransportChannel.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreographyLocation, LocationSet};
#[derive(ChoreographyLocation)]
struct Alice;
use chorus_lib::transport::local::LocalTransportChannelBuilder;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).build();
use chorus_lib::transport::local::{LocalTransport};

let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
}

Because of the nature of the Local transport, you must use the same LocalTransportChannel instance for all locations. You can clone the LocalTransportChannel instance and pass it to each Projector::new constructor.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
use std::thread;
use chorus_lib::core::{ChoreographyLocation, ChoreoOp, Choreography, Projector, LocationSet};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice, Bob);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
    }
}
let transport_channel = LocalTransportChannelBuilder::new()
    .with(Alice)
    .with(Bob)
    .build();
let mut handles: Vec<thread::JoinHandle<()>> = Vec::new();
{
    // create a transport for Alice
    let transport = LocalTransport::new(Alice, transport_channel.clone());
    handles.push(thread::spawn(move || {
        let p = Projector::new(Alice, transport);
        p.epp_and_run(HelloWorldChoreography);
    }));
}
{
    // create another for Bob
    let transport = LocalTransport::new(Bob, transport_channel.clone());
    handles.push(thread::spawn(move || {
        let p = Projector::new(Bob, transport);
        p.epp_and_run(HelloWorldChoreography);
    }));
}
}

The HTTP Transport

The http transport is used to execute choreographies on different machines. This is useful for executing choreographies in a distributed system.

To use the http transport, import HttpTransport and HttpTransportConfigBuilder from the chorus_lib crate.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::transport::http::{HttpTransport, HttpTransportConfigBuilder};
}

We need to construct a HttpTransportConfig using the HttpTransportConfigBuilder. First, we specify the target location and the hostname and port to listen on using the for_target method. Then, we specify the other locations and their (hostname, port) pairs using the with method.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
use chorus_lib::transport::http::{HttpTransport, HttpTransportConfigBuilder};
// `Alice` listens on port 8080 on localhost
let config = HttpTransportConfigBuilder::for_target(Alice, ("localhost", 8080))
                // Connect to `Bob` on port 8081 on localhost
                .with(Bob, ("localhost", 8081))
                .build();
let transport = HttpTransport::new(config);
}

In the above example, the transport will start the HTTP server on port 8080 on localhost. If Alice needs to send a message to Bob, it will use http://localhost:8081 as the destination.

Creating a Custom Transport

You can also create your own transport by implementing the Transport trait. It might be helpful to first build a TransportConfig to have the the information that you need for each ChoreographyLocation, and then have a constructor that takes the TransportConfig and builds the Transport based on it. While the syntax is similar to HttpTransportConfig, which is HttpTransportConfigBuilder::for_target(target_location, target_information), chained with information about other locations using the .with(other_location, other_location_information), the type of information for each ChoreographyLocation might diverge from the (host_name, port) format presented in HttpTransport. In some cases, the target_information could even have a different type than the following other_location_information types. But all the other_location_informations should have the same type.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
use chorus_lib::transport::TransportConfigBuilder;
let config = TransportConfigBuilder::for_target(Alice, ())
                .with(Bob, ("localhost", 8081))
                .with(Carol, ("localhost", 8082))
                .build();
}

See the API documentation for more details.

Note on the location set of the Choreography

Note that when calling epp_and_run on a Projector, you will get a compile error if the location set of the Choreography is not a subset of the location set of the Transport. In other words, the Transport should have information about every ChoreographyLocation that Choreography can talk about. So this will fail:

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
use chorus_lib::core::{ChoreographyLocation, Projector, Choreography, ChoreoOp, LocationSet};

#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
     type L = LocationSet!(Alice, Bob);
     fn run(self, op: &impl ChoreoOp<Self::L>) {
     }
}

let transport_channel = LocalTransportChannelBuilder::new().with(Alice).build();
let transport = LocalTransport::new(Alice, transport_channel.clone());
let projector = Projector::new(Alice, transport);
projector.epp_and_run(HelloWorldChoreography);
}

Projector

Projector is responsible for performing the end-point projection and executing the choreography.

Creating a Projector

To create a Projector, you need to provide the target location and the transport.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
use chorus_lib::core::{ChoreographyLocation, Projector, LocationSet};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let projector = Projector::new(Alice, alice_transport);
}

Notice that the Projector is parameterized by its target location type. You will need one projector for each location to execute choreography.

Executing a Choreography

To execute a choreography, you need to call the epp_and_run method on the Projector instance. The epp_and_run method takes a choreography, performs the end-point projection, and executes the choreography.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
use chorus_lib::core::{ChoreographyLocation, Projector, Choreography, ChoreoOp, LocationSet};
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
struct HelloWorldChoreography;
impl Choreography for HelloWorldChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
    }
}
let projector = Projector::new(Alice, alice_transport);
projector.epp_and_run(HelloWorldChoreography);
}

If the choreography has a return value, the epp_and_run method will return the value. We will discuss the return values in the Input and Output section.

Input and Output

To use ChoRus as part of a larger system, you need to be able to write a choreography that takes input and returns output.

Moreover, you may want to write a choreography that takes located values as input and returns located values as output. In such cases, you need to be able to construct/unwrap located values outside of the run method.

In this section, we will show you how to write a choreography that takes input and returns output.

Input

To take input, you can use fields of the struct that implements the Choreography trait. For example, the following choreography takes a String as input.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());

struct DemoChoreography {
    input: String,
}

impl Choreography for DemoChoreography {
    type L = LocationSet!();
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        println!("Input: {}", self.input);
    }
}
}

You can construct an instance of the choreography with the input and pass it to the epp_and_run function.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography {
    input: String,
}
impl Choreography for DemoChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        println!("Input: {}", self.input);
    }
}

let choreo = DemoChoreography {
    input: "World".to_string(),
};

let projector = Projector::new(Alice, alice_transport);
projector.epp_and_run(choreo);
}

Located Input

Input of normal types such as String must be available at all locations. However, you may want to take input that is only available at a single location. You can do so by using a located value as a field of the choreography struct.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography {
    input: Located<String, Alice>,
}

impl Choreography for DemoChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        op.locally(Alice, |un| {
            let input = un.unwrap(&self.input);
            println!("Input at Alice: {}", input);
        });
    }
}
}

Because the input field is located at Alice, you can only access the string at Alice using the Unwrapper.

To construct this choreography, you must pass a located value. The Projector struct provides two methods to construct located values: local and remote.

local constructs a located value that is available at the projection target. You must provide an actual value as an argument. The location will be the same as the target of the projector.

remote constructs a located value that is available at a different location. You must provide a location of the value. Note that this location must be different from the target of the projector. As of now, ChoRus does not check this at compile time. If you pass the same location as the target of the projector, the program will panic at runtime.

To run the sample choreography above at Alice, we use the local method to construct the located value.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography {
    input: Located<String, Alice>,
}

impl Choreography for DemoChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        op.locally(Alice, |un| {
            let input = un.unwrap(&self.input);
            println!("Input at Alice: {}", input);
        });
    }
}
let projector_for_alice = Projector::new(Alice, alice_transport);
// Because the target of the projector is Alice, the located value is available at Alice.
let string_at_alice: Located<String, Alice> = projector_for_alice.local("Hello, World!".to_string());
// Instantiate the choreography with the located value
let choreo = DemoChoreography {
    input: string_at_alice,
};
projector_for_alice.epp_and_run(choreo);
}

For Bob, we use the remote method to construct the located value.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography {
    input: Located<String, Alice>,
}

impl Choreography for DemoChoreography {
    type L = LocationSet!(Alice, Bob);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        op.locally(Alice, |un| {
            let input = un.unwrap(&self.input);
            println!("Input at Alice: {}", input);
        });
    }
}
let projector_for_bob = Projector::new(Bob, bob_transport);
// Construct a remote located value at Alice. The actual value is not required.
let string_at_alice = projector_for_bob.remote(Alice);
// Instantiate the choreography with the located value
let choreo = DemoChoreography {
    input: string_at_alice,
};
projector_for_bob.epp_and_run(choreo);
}

Output

Similarly, we can get output from choreographies by returning a value from the run method.

To do so, we specify the output type to the Choreography trait and return the value of the type from the run method.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography;

impl Choreography<String> for DemoChoreography {
    type L = LocationSet!();
    fn run(self, op: &impl ChoreoOp<Self::L>) -> String {
        "Hello, World!".to_string()
    }
}
}

epp_and_run returns the value returned from the run method.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography;

impl Choreography<String> for DemoChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) -> String {
        "Hello, World!".to_string()
    }
}
let choreo = DemoChoreography;
let projector = Projector::new(Alice, alice_transport);
let output = projector.epp_and_run(choreo);
assert_eq!(output, "Hello, World!".to_string());
}

Located Output

You can use the Located<V, L1> as a return type of the run method to return a located value. The projector provides a method unwrap to unwrap the output located values.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography;

impl Choreography<Located<String, Alice>> for DemoChoreography {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<String, Alice> {
        op.locally(Alice, |_| {
            "Hello, World!".to_string()
        })
    }
}

let projector = Projector::new(Alice, alice_transport);
let output = projector.epp_and_run(DemoChoreography);
let string_at_alice = projector.unwrap(output);
assert_eq!(string_at_alice, "Hello, World!".to_string());
}

Because projectors are parametric over locations, you can only unwrap located values at the target location.

You can return multiple located values by returning a tuple or struct that contains multiple located values. They don't have to be located at the same location, but you can only unwrap them at the correct location.

Higher-order Choreography

Higher-order choreography is a choreography that takes another choreography as an argument. Just like higher-order functions, higher-order choreographies are useful for abstracting over common patterns.

This section describes how to define and execute higher-order choreographies.

Defining a Higher-order Choreography

To define a higher-order choreography, you need to create a generic struct that takes a type parameter that implements the Choreography trait.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct HigherOrderChoreography<C: Choreography> {
    sub_choreo: C,
};
}

When you implement the Choreography trait, you have access to the sub_choreo field. You can use the call method to execute the sub-choreography.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct HigherOrderChoreography<C: Choreography> {
    sub_choreo: C,
};
impl<C: Choreography<(), L = LocationSet!(Alice, Bob)>> Choreography for HigherOrderChoreography<C> {
    type L = LocationSet!(Alice, Bob);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        op.call(self.sub_choreo);
    }
}
}

Passing values to a sub-choreography

It is often useful to pass values to a sub-choreography. To do so, instead of storing the sub-choreography object as a field, you associate the sub-choreography trait with the choreography using the std::marker::PhantomData type.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
use std::marker::PhantomData;

trait SubChoreography {
    fn new(arg: Located<i32, Alice>) -> Self;
}

struct HigherOrderChoreography<C: Choreography<Located<bool, Alice>> + SubChoreography> {
    _marker: PhantomData<C>,
};

impl<C: Choreography<Located<bool, Alice>, L = LocationSet!(Alice)> + SubChoreography> Choreography for HigherOrderChoreography<C> {
    type L = LocationSet!(Alice);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        let num_at_alice = op.locally(Alice, |_| {
            42
        });
        let sub_choreo = C::new(num_at_alice);
        op.call(sub_choreo);
    }
}
}

Here, the HigherOrderChoreography struct takes a type parameter C that implements both the Choreography trait and the SubChoreography trait. The SubChoreography trait ensures that the C type can be constructed with a located integer at Alice using the new constructor.

Location Polymorphism

Another feature of ChoRus is location polymorphism. Location polymorphism allows choreographies to be defined using generic locations. These locations can then be instantiated with concrete locations, allowing the choreography to be executed on different locations.

To define a location-polymorphic choreography, you need to create a generic struct that takes a type parameter that implements the ChoreographyLocation trait. When instantiating the choreography, you can pass a concrete location.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct LocationPolymorphicChoreography<L1: ChoreographyLocation> {
    location: L1,
}

impl<L1: ChoreographyLocation> Choreography for LocationPolymorphicChoreography<L1> {
    type L = LocationSet!(L1);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        op.locally(self.location, |_| {
            println!("Hello, World!");
        });
    }
}

let alice_say_hello = LocationPolymorphicChoreography {
    location: Alice,
};
let bob_say_hello = LocationPolymorphicChoreography {
    location: Bob,
};
}

Choreographic Enclave and Efficient Conditional

ChoRus supports the enclave operator to achieve efficient conditional execution.

Conditional with Broadcast

Consider the following protocol:

  1. Alice generates a random number x and sends it to Bob.
  2. Bob checks if x is even. If it is even, Bob sends x to Carol. Otherwise, Bob terminates.

This protocol can be implemented as follows:

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
fn get_random_number() -> u32 {
  42 // for presentation purpose
}

struct DemoChoreography;

impl Choreography for DemoChoreography {
    type L = LocationSet!(Alice, Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        let x_at_alice = op.locally(Alice, |_| {
            get_random_number()
        });
        let x_at_bob = op.comm(Alice, Bob, &x_at_alice);
        let is_even_at_bob: Located<bool, Bob> = op.locally(Bob, |un| {
            let x = un.unwrap(&x_at_bob);
            x % 2 == 0
        });
        let is_even: bool = op.broadcast(Bob, is_even_at_bob);
        if is_even {
            let x_at_carol = op.comm(Bob, Carol, &x_at_bob);
            op.locally(Carol, |un| {
                let x = un.unwrap(&x_at_carol);
                println!("x is even: {}", x);
            });
        }
    }
}
}

While this code correctly implements the protocol, it is inefficient. The is_even value is broadcasted to all locations, but Alice does not need to receive the value. Ideally, we want to send is_even_at_bob only to Carol and branch only on Bob and Carol.

In ChoRus, we can achieve this using the enclave operator. First, let us define a sub-choreography that describes the communication between Bob and Carol:

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct BobCarolChoreography {
    x_at_bob: Located<u32, Bob>,
};
impl Choreography for BobCarolChoreography {
    type L = LocationSet!(Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        let is_even_at_bob: Located<bool, Bob> = op.locally(Bob, |un| {
            let x = un.unwrap(&self.x_at_bob);
            x % 2 == 0
        });
        let is_even: bool = op.broadcast(Bob, is_even_at_bob);
        if is_even {
            let x_at_carol = op.comm(Bob, Carol, &self.x_at_bob);
            op.locally(Carol, |un| {
                let x = un.unwrap(&x_at_carol);
                println!("x is even: {}", x);
            });
        }
    }
}
}

Notice that BobCarolChoreography only describes the behavior of Bob and Carol (see its location set L). enclave is an operator to execute a choreography only at locations that is included in the location set. In this case, if we invoke BobCarolChoreography with enclave in the main choreography, it will only be executed at Bob and Carol and not at Alice.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
fn get_random_number() -> u32 {
  42 // for presentation purpose
}
struct BobCarolChoreography {
    x_at_bob: Located<u32, Bob>,
};
impl Choreography for BobCarolChoreography {
    type L = LocationSet!(Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        let is_even_at_bob: Located<bool, Bob> = op.locally(Bob, |un| {
            let x = un.unwrap(&self.x_at_bob);
            x % 2 == 0
        });
        let is_even: bool = op.broadcast(Bob, is_even_at_bob);
        if is_even {
            let x_at_carol = op.comm(Bob, Carol, &self.x_at_bob);
            op.locally(Carol, |un| {
                let x = un.unwrap(&x_at_carol);
                println!("x is even: {}", x);
            });
        }
    }
}
struct MainChoreography;
impl Choreography for MainChoreography {
    type L = LocationSet!(Alice, Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        let x_at_alice = op.locally(Alice, |_| {
            get_random_number()
        });
        let x_at_bob = op.comm(Alice, Bob, &x_at_alice);
        op.enclave(BobCarolChoreography {
            x_at_bob,
        });
    }
}
}

Returning Values from Enclave

Just like the call operator, the enclave operator can return a value. However, the type of the returned value must implement the Superposition trait. Superposition provides a way for ChoRus to construct a value on locations that are not specified in the enclave operator.

In general, Superposition is either a located value or a struct consisting only of located values. The Located struct implements the Superposition trait, so you can return located values without any code. If you wish to return a struct of located values, you need to derive the Superposition trait using the derive macro.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
fn get_random_number() -> u32 {
  42 // for presentation purpose
}

#[derive(Superposition)]
struct BobCarolResult {
    is_even_at_bob: Located<bool, Bob>,
    is_even_at_carol: Located<bool, Carol>,
}

struct BobCarolChoreography {
    x_at_bob: Located<u32, Bob>,
};

impl Choreography<BobCarolResult> for BobCarolChoreography {
    type L = LocationSet!(Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) -> BobCarolResult {
        let is_even_at_bob: Located<bool, Bob> = op.locally(Bob, |un| {
            let x = un.unwrap(&self.x_at_bob);
            x % 2 == 0
        });
        let is_even: bool = op.broadcast(Bob, is_even_at_bob.clone());
        if is_even {
            let x_at_carol = op.comm(Bob, Carol, &self.x_at_bob);
            op.locally(Carol, |un| {
                let x = un.unwrap(&x_at_carol);
                println!("x is even: {}", x);
            });
        }
        BobCarolResult {
            is_even_at_bob,
            is_even_at_carol: op.locally(Carol, |_| is_even),
        }
    }
}

struct MainChoreography;

impl Choreography for MainChoreography {
    type L = LocationSet!(Alice, Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) {
        let x_at_alice = op.locally(Alice, |_| {
            get_random_number()
        });
        let x_at_bob = op.comm(Alice, Bob, &x_at_alice);
        let BobCarolResult {
            is_even_at_bob,
            is_even_at_carol,
        } = op.enclave(BobCarolChoreography {
            x_at_bob,
        });
        // can access is_even_at_bob and is_even_at_carol using `locally` on Bob and Carol
    }
}
}

Runner

Runner is a struct that can be used to run a Choreography without doing end-point projection. It gives semantics to the Choreography and allows it to be run in a way that is similar to a function call.

To use Runner, construct an instance using the new constructor, and then call the run method with the Choreography.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct DemoChoreography;
impl Choreography for DemoChoreography {
    type L = LocationSet!();
    fn run(self, op: &impl ChoreoOp<Self::L>) {
    }
}
let runner = Runner::new();
runner.run(DemoChoreography);
}

As described in the Input and Output section, Runner can also pass values to the Choreography and receive values from it.

Because Runner executes the Choreography at all locations, all located inputs must be provided. Also, Runner can unwrap any located values returned by the Choreography.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, Superposition, Runner, LocationSet};
use chorus_lib::transport::local::{LocalTransport, LocalTransportChannelBuilder};
#[derive(ChoreographyLocation)]
struct Alice;
#[derive(ChoreographyLocation)]
struct Bob;
#[derive(ChoreographyLocation)]
struct Carol;
let transport_channel = LocalTransportChannelBuilder::new().with(Alice).with(Bob).with(Carol).build();
let alice_transport = LocalTransport::new(Alice, transport_channel.clone());
let bob_transport = LocalTransport::new(Bob, transport_channel.clone());
let carol_transport = LocalTransport::new(Carol, transport_channel.clone());
struct SumChoreography {
    x_at_alice: Located<u32, Alice>,
    y_at_bob: Located<u32, Bob>,
}
impl Choreography<Located<u32, Carol>> for SumChoreography {
    type L = LocationSet!(Alice, Bob, Carol);
    fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<u32, Carol> {
        let x_at_carol = op.comm(Alice, Carol, &self.x_at_alice);
        let y_at_carol = op.comm(Bob, Carol, &self.y_at_bob);
        op.locally(Carol, |un| {
            let x = un.unwrap(&x_at_carol);
            let y = un.unwrap(&y_at_carol);
            x + y
        })
    }
}

let runner = Runner::new();
let x_at_alice = runner.local(1);
let y_at_bob = runner.local(2);
let sum_at_carol = runner.run(SumChoreography {
    x_at_alice,
    y_at_bob,
});
assert_eq!(runner.unwrap(sum_at_carol), 3);
}

Links

Here are some links you might find useful: