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!"); }); } } }