Client Communication

In the previous chapter, we defined a hello provider and then run it immediately using run_session_with_result. However in practical applications, our Ferrite programs will typically have more complex session types, and we would want to run multiple programs in parallel that communicate with each others.

To demonstrate that, instead of running hello_provider directly, we can define a hello_client that communicates with a provider of the Hello protocol, and then link it with hello_provider.

First, we define hello_client to have the following type:

let hello_client: Session<ReceiveChannel<Hello, End>> = ...

Our hello_client has the Rust type Session<ReceiveChannel<Hello, End>>, indicating that it is a Ferrite program offering the session type ReceiveChannel<Hello, End>. Here we encounter a new session type in the form ReceiveChannel<A, B>, which is used to receive a channel of session type A offered by some provider, and then continue offering session type B.

Channels

Ferrite allows sending and receiving of channels, which represent the client end point to communicate with a provider that offers the channel's session type. At high level, we can think of receiving a channel using ReceiveChannel<A, B> is similar to plain Rust functions with function traits like FnOnce(A) -> B. In other words, ReceiveChannel is the equivalent of function types in Ferrite.

There is however one important distinction of ReceiveChannel from regular Rust functions. That is channels received from ReceiveChannel must be used linearly, i.e. they cannot be ignored or dropped. We will revisit this property in later chapters to see why linearity is important, and why bare Rust code is not sufficient to support linearity.

Now back to our hello_client example. The session type ReceiveChannel<Hello, End> indicates that hello_client is receiving a channel of session type Hello, and then terminates with End. To implement such a session type, we can implement hello_client as follows:

  let hello_client: Session<ReceiveChannel<Hello, End>> =
    receive_channel(|a| {
      receive_value_from(a, move |greeting| {
        println!("Received greetings from provider: {}", greeting);
        wait(a, terminate())
      })
    });

Our hello_client body looks slightly more complicated than hello_provider. To understand what's going on, we will go through each line of hello_client's body. Starting with receive_channel:

receive_channel(|provider| { ... })

To match the final session type ReceiveChannel<Hello, End> that is offered by hello_client, we use receive_channel to receive a channel of session type Hello, and then binds it to the channel variable a. This is similar to a Rust function accepting an argument and bind it to a variable.

Inside the continuation ..., since we have received the Hello channel already, we will continue to offer the session type End. To do that, we just need to eventually terminate hello_client. However we cannot terminate hello_client just yet, because the channel variable a is linear, and we must fully consume it before we can terminate.

Recall that Hello is a type alias, so the actual session type of the channel variable a is SendValue<String, End>. But instead of having to offer that, we are acting as the client to consume the session type SendValue<String, End>. Since the provider is expected to send a String value, as a client we are expected to receive a String value from the provider. We can do that using receive_value_from:

receive_value_from(provider, move |greeting| {
  println!("Received greetings from provider: {}", greeting);
  ...
})

We use receive_value_from to receive a value sent from the a channel, and then bind the received String value to the Rust variable greeting. We then print out the value of greeting using println!. Following that, in the continuation ..., the session type of a changes from SendValue<String, End> to become End.

Unlike regular Rust variables, each time we interacts with a channel variable, the session type of the channel variable is updated to its continuation session type. Since Ferrite channels are linear, we have to continuously interact with the channel until it is fully terminated.

After calling receive_value_from, we have the channel variable a with session type End, and we need to offer the session type End by terminating. But we can't terminate just yet, because End simply indicates that the provider will eventually terminates, but may not yet been terminated. Hence we would first have to wait for a to terminate using wait:

wait(provider, terminate())

We use wait to wait for the provider on the other side of a channel to terminate. After that, the a channel is discarded, and we don't have anymore unused channel variable. With that, we can finally terminate our program using terminate().

Linking Provider With Client

We have now defined a hello_client program that can accept channel from any provider offering the session type Hello. In theory, we can call hello_client with any provider that offers Hello, not just hello_provider.

To link hello_client specifically with hello_provider, we have to explicitly ask Ferrite to perform the linking. This can be done using apply_channel:

  let main: Session<End> = apply_channel(hello_client, hello_provider);

The apply_channel construct is provided by Ferrite to link a client Ferrite program of session type ReceiveChannel<A, B> with a provider Ferrite program of session type A, resulting in a new Ferrite program of session type B as the result.

We can think of the form apply_channel(f, x) as being similar similar to regular function application in the form f(x). With f having the Rust type FnOnce(A) -> B and x having the Rust type A, the result type of applying x to func would be B.

Run Session

After applying hello_provider to hello_client, we now have a single main program. When main is executed, it will run both hello_provider and hello_client in parallel, and establish a communication channel between the two processes.

Since main has the session type End, we can use the Ferrite construct run_session to run the program:

  run_session(main).await;

run_session accepts any Ferrite program offering the session type End, executes the program, and wait for it to terminate. run_session is similar to run_session_with_result, other than it does not expect the Ferrite program to send back any Rust value as the result.

Full Program

Putting everything together, we now have our second hello world program that is made of a hello_provider and a hello_client communicating with each others.

use ferrite_session::prelude::*;

type Hello = SendValue<String, End>;

#[tokio::main]
async fn main()
{
  let hello_provider: Session<Hello> =
    send_value("Hello World!".to_string(), terminate());

  let hello_client: Session<ReceiveChannel<Hello, End>> =
    receive_channel(|a| {
      receive_value_from(a, move |greeting| {
        println!("Received greetings from provider: {}", greeting);
        wait(a, terminate())
      })
    });

  let hello_client_partial: PartialSession<
    HList![SendValue<String, End>],
    End,
  > = receive_value_from(Z, move |greeting| {
    println!("Received greetings from provider: {}", greeting);
    wait(Z, terminate())
  });

  let main: Session<End> = apply_channel(hello_client, hello_provider);

  run_session(main).await;
}