Hole Driven Development

Just like learning any new programming language, it may be overwhelming to write your first session type program in Ferrite. To help you unstuck when encountering compile errors, we encourage the use of typed holes to discover what is needed to complete your program.

The use of typed holes are common in languages like Haskell, Agda, and Idris. In Rust, there isn't officially support for using typed holes, but we can somewhat emulate the feature using techniques described in this chapter.

Todo Placeholder

Suppose we are a total beginner again, and we want to write our first hello world provider. We start by defining the session type we want, which is SendValue<String, End>, and we can then write down the outline for our Ferrite program as follows:

let hello_provider: Session<SendValue<String, End>> = todo!();

The todo! macro allows us to put a placeholder in unfinished Rust code so that we can try and compile the code and see if there is any type error. By writing our code step by step and filling the blank with todo!(), we can narrow down the potential places where our code is incorrect.

At this stage, we should be able to compile our program with no error. This shows that the protocol that we have defined, SendValue<String, End>, is a valid session type. If we have gotten a compile error otherwise, it could have been caused by us trying to write an invalid protocol like SendValue<String, String>.

When using todo!(), the Rust compiler might emit warnings about unreacheable code. Since we are still writing our program, we can add the #![allow(unreachable_code)] pragma to disable the warning temporarily until we finish writing the code.

Unit Placeholder

Now that we have defined the outline, we can move to the next step and try to write the first step. By looking at the first part SendValue<String, ...>, we know that we have to send a string value. But we might not know what should be done after sending the value, so we could write something like follows:

let hello_provider: Session<SendValue<String, End>> =
  send_value("Hello World!".to_string(), todo!());

We can try to compile our code again, and Rust will accept the code we have written. However the use of todo!() does not tell us how we should continue our program. In languages like Haskell, we could have used _ instead of todo!(), and GHC would tell us what should be the type of the expression in the hole. In Rust, we would instead use the unit type () to deliberately cause a compile error:

let hello_provider: Session<SendValue<String, End>> =
  send_value("Hello World!".to_string(), ());

Now if we compile our code, we would get a compile error from Rust that looks like follows:

error[E0308]: mismatched types
 |
 |       send_value("Hello World!".to_string(), ());
 |                                              ^^ expected struct `PartialSession`, found `()`
 |
 = note: expected struct `PartialSession<(), End>`
         found unit type `()`

With this compile error, we can know that we are supposed to fill in the hole with Rust expression that has the type PartialSession<(), End>, or Session<End>.

Sometimes we may also intuitively think of a type that should be in a hole. In such case, we can also use the todo!() as T pattern to verify if our intuition is correct. So we can for example write:

let hello_provider: Session<SendValue<String, End>> =
  send_value("Hello World!".to_string(), todo!() as Session<End>);

And our code will compile successfully. If we were to annotate it with an invalid type, such as todo()! as Session<ReceiveValue<String, End>> again, Rust will also return a compile error.

Now that we know the continuation needs to have the type Session<End>, we can then fill in the blank with terminate() and complete our program.

Underscores

Now that we have finished hello_provider, we can try implementing hello_client once again. Supposed we know that the first expression to match ReceiveChannel<...> is receive_channel, but we don't know what would be in the linear context, we can write our program as follows:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    todo!() as PartialSession<HList![_], End>
  });

The code above will still compile successfully with Rust, so we know that in our continuation, we have one element in the linear context as denoted by HList![_], but we don't yet know what it is. Rust allows us to use _ as a wildcard in places where it can infer the type, such as the case when we use it in todo!() as .... Using this technique, we can also slowly narrow down our type definition and not get overwhelmed by an overly long error message.

Now we can try and fill in the _ with a bogus protocol like End and see what happens:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    todo!() as PartialSession<HList![End], End>
  });

Now if we try to compile our program again, we would get a compile error that looks like follows:

error[E0271]: type mismatch resolving `<() as AppendContext<(SendValue<String, End>, ())>>::Appended == (End, ())`
 |
 |       receive_channel(move |a| {
 |       ^^^^^^^^^^^^^^^ expected struct `SendValue`, found struct `End`
 |
 = note: expected type `(SendValue<String, End>, ())`
           found tuple `(End, ())`

The key information is in the last two lines, which states that there is a type mismatch between the expected type (SendValue<String, End>, ()) and the actual type (End, ()). Recall from previous chapter that this is the desugared versions of the expected list HList![SendValue<String, End>] and the actual list HList![End]. Using this information, we are able to fill in the correct type for our hole:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    todo!() as PartialSession<HList![SendValue<String, End>], End>
  });

When writing complex session type programs, we can also use _ in places where we do not care about the actual type. For example, we may be interested to know whether the there is one channel in the linear context in the form SendValue<String, ...>. In that can we can also write our code as follows:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    todo!() as PartialSession<HList![SendValue<String, _>], _>
  });

Now that we know the first session type in the linear context is in the form SendValue<String, ...>, we can attempt to write the next part of our program. However suppose that we forgot that the behavior of protocols is inverted on the client side, we may try to write a program that sends a value to channel a instead of receiving from it:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    send_value_to(a, "Hello World!".to_string(), todo!())
  });

If we try to compile the code, we would get an error such as follows, which is the same error as previously described:

error[E0277]: the trait bound `Z: ContextLens<(SendValue<String, End>, ()), ReceiveValue<String, _>, _>` is not satisfied
 |
 |         send_value_to(a, "Hello World!".to_string(), todo!())
 |         ^^^^^^^^^^^^^ the trait `ContextLens<(SendValue<String, End>, ()), ReceiveValue<String, _>, _>` is not implemented for `Z`
 |

With the help of todo!(), we are able to spot the type errors in our program before finish writing the whole program. It saves us the effort of figuring out what to fill in the blank inside send_value_to(a, "Hello World!".to_string(), ...), as our program is incorrect anyway.

Comment

Sometimes we may have written too much code, and by the time we try to compile our program, we get a big list of compile errors that are difficult to decipher. In such case, it may be helpful to comment out part of our Ferrite program and replace them with todo!(), to find out the initial place where the error occured.

For example, suppose we finished writing the wrong implementation of hello_client, we can debug our code by commenting out the code to become something like:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    todo!()
    // send_value_to(a, "Hello World!".to_string(),
    //   wait(a, terminate()))
  });

If we build our program at this step, the compilation will be successful again. We can then move forward to the next step of our program while still commenting out the remaining part:

let hello_client: Session<ReceiveChannel<SendValue<String, End>, End>> =
  receive_channel(move |a| {
    send_value_to(a, "Hello World!".to_string(),
      todo!())
    //   wait(a, terminate()))
  });

At this step, we would then get a compile error. We can then know that the cause of the error is from our use of send_value_to, and is unaffected by the remaining expression wait(a, terminate()) because they have been commented out and replaced with todo!().

Rust IDE

When developing Ferrite programs, it may be helpful to use Rust IDEs such as Rust Analayzer. The IDE would show the hints on the types of the variables and functions. Using and IDE, we can for example easily see that the type of channel variables, such as a in hello_client has the type Z. This can be useful especially for beginners, who may be unfamiliar with the various types defined in Ferrite.