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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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!");
}

Multicast

The multicast operator is similar to the broadcast operator, but it allows you to manually specify the recipients instead of sending the value to all locations. The operator returns a MultiplyLocated value that is available at the all recipient locations.

#![allow(unused)]
fn main() {
extern crate chorus_lib;
use chorus_lib::core::{ChoreoOp, Choreography, ChoreographyLocation, Projector, Located, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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, Carol);
    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 and Carol
let num_at_bob_and_carol: MultiplyLocated<i32, LocationSet!(Bob, Carol)> =
    op.multicast(Alice, <LocationSet!(Bob, Carol)>::new(), &num_at_alice);

// Bob and Carol can now access the value
op.locally(Bob, |un| {
    let num_at_bob: &i32 = un.unwrap(&num_at_bob_and_carol);
    println!("The number at Bob is {}", num_at_bob);
});
op.locally(Carol, |un| {
    let num_at_carol: &i32 = un.unwrap(&num_at_bob_and_carol);
    println!("The number at Carol is {}", num_at_carol);
});
    }
}
}

The second parameter of the multicast is a value of the LocationSet type. You can use the new() method of the LocationSet type to obtain a value representation of the location set.

Both Bob and Carol can access the value sent from Alice inside their local computation using the same unwrap method.

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, MultiplyLocated, Runner, LocationSet, Serialize, Deserialize};
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!");
        });
    }
}
}