Channel Selectors
In the code for hello_client
, we have seen
that the receive_channel
construct introduces a new channel variable to our continuation,
which we bind to the variable a
. The channel variable a
is then used in both
receive_value_from(a, ...)
and wait(a, ...)
to access the given channel in the linear context.
In this chapter, we will peek a bit into the mechanics of how the channel variables
provide access to channels in the linear context.
Context Lenses
Recall from the previous chapter that a linear context
has the form HList![A0, A1, ...]
, with each element implementing Protocol
.
During the execution of a Ferrite program, the session type of the channels
in the linear context will be updated in each step, until the final linear
context has the form HList![Empty, Empty, ...]
to be safely terminated.
When some Ferrite constructs such as receive_value_from
are called, a channel
variable needs to be given so that the construct can know which channel
in the linear context it should access. Since the linear context is represented
as a type level list, the channels are in fact not named in the linear context.
Instead, channel variables such as a
in hello_client
actually have types
that implement context lenses to access channels in the linear context
by position.
Conceptually, a type N
that implements the ContextLens
trait provides
access to a particular element in the type level list, and update it
to a new type. For example in hello_client
, the type of a
implements
ContextLens
for accessing the linear contexts
HList![SendValue<String, End>]
, and updates it to HList[End]
, then
finally updates it to HList![Empty]
.
Type Level Natural Numbers
To define types that implement the ContextLens
trait, Ferrite uses
type level natural numbers to implement the access to channels
in the type level list by the corresponding position. A type level
natural number starts with the type Z
, which corresponds to the
number 0. Following that, we have the type S<Z>
, which means
the successor of Z
, to correspond to the number 1.
Similarly, we have S<S<Z>>
corresponding to the successor of 1,
which is 2, and so on. So we have the types
Z
, S<Z>
, S<S<Z>>
, S<S<S<Z>>>
, ... corresponding to
the number sequence 0, 1, 2, 3, ...
For each of the type level natural numbers, Ferrite implements
the ContextLens
trait for accessing channels at the corresponding
zero-indexed position in the linear context. So for example:
-
The type
Z
can update any linear context in the formHList![A0, ...]
toHList![B, ...]
, with the first elementA0
replaced withB
and the remaining elements unchanged. -
The type
S<Z>
can update any linear context in the formHList![A0, A1, ...]
toHList![A0, B, ...]
, with the second elementA1
replaced withB
and the remaining elements unchanged. -
The type
S<S<Z>>
can update any linear context in the formHList![A0, A1, A2, ...]
toHList![A0, A1, B, ...]
, with the third elementA3
replaced withB
and the remaining elements unchanged.
As demonstration, we can deduce that the channel variable a
in hello_client
has
the type Z
, because there is only one channel in the linear context and it is
the one that we are interested in. Z
implements the ContextLens
for updating
the first element in any linear context. So receive_channel_from
can use Z
to update the linear context from HList![ReceiveValue<String, End>]
to HList![End]
,
and it can also use Z
to update the linear context from HList![End]
to HList![Empty]
.
The ContextLens
Trait
The full definition of the ContextLens
trait is in the form ContextLens<C1, A, B, Target=C2>
.
It means that a type N
that implements ContextLens<C1, A, B, Target=C2>
provides
the operation to access a channel of session type A
that can be found in C1
,
update it to B
, and result in the new context C2
. This can be elaborated with
the implementations by the natural numbers:
-
Z: ContextLens<HList![A0, ...], A0, B, Target=HList![B, ...]>
-
S<Z>: ContextLens<HList![A0, A1, ...], A0, B, Target=HList![A0, B, ...]>
-
S<S<Z>>: ContextLens<HList![A0, A1, A2,...], A0, B, Target=HList![A0, A1, B, ...]>
Following the above template, we can know that in the case of hello_client
, the type Z
does implements the required ContextLens
traits:
-
Z: ContextList< HList![ReceiveValue<String, End>], ReceiveValue<String, End>, End, Target=HList![End]>
-
Z: ContextList< HList![End], End, Empty, Target=HList![Empty]>
Writing Partial Ferrite Programs
Now that we know how context lenses are actually implemented, it would in fact
be possible for us to write partial Ferrite programs of type PartialSession
,
by hard coding the specific context lenses to access the channels in the linear context.
For example, we could have written a partial version of hello_client
as follows:
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())
});
While writing such program is possible, the code is not ergonomic and is generally not recommended. This is because we are hardcoding the position of the channels in the linear context, instead of using meaningful identifiers. This would also reduce readability and makes it more difficult to add or remove channels to the program.
This style of programming using the position is similar to programming using De Bruijn index. While this is useful for internal implementation, we as programmers tend to prefer using named variables to improve readability and better structure our code.
Type Errors Using Context Lenses
When we write Ferrite programs that contain invalid access to channels in the linear context,
we may get compile errors showing that types like Z
do not implement a particular context lens.
As an example, consider the following incorrect version of hello_client
that tries to
send a value to a
instead of receiving from it:
let hello_client_incorrect
: Session<ReceiveChannel<SendValue<String, End>, End>>
= receive_channel(|a| {
send_value_to(a, "Hello World!".to_string(),
wait(a, terminate()))
});
If we try to compile the program, we would get an error message similar to the following:
error[E0277]: the trait bound `Z: ContextLens<(SendValue<String, End>, ()), ReceiveValue<String, _>, _>` is not satisfied
|
20 | send_value_to(a, "Hello World!".to_string(),
| ^^^^^^^^^^^^^ the trait `ContextLens<(SendValue<String, End>, ()), ReceiveValue<String, _>, _>` is not implemented for `Z`
|
|
49 | N: ContextLens<C, ReceiveValue<T, B>, B>,
| ------------------------------------- send_value_to`
|
= help: the following implementations were found:
<Z as ContextLens<(A1, C), A1, A2>>
The key meaning for the error message above is that the type Z
does not implement
ContextLens<HList![SendValue<String, End>], ReceiveValue<String, End>, End>
.
This is because although Z
implements access to the first element of any
non-empty linear context, the first element in the linear context of hello_client
has the protocol SendValue<String, End>
, but the call to send_value_to(a, ...)
requires the first element of the linear context to have the protocol
ReceiveValue<String, End>
.
Error messages such as above may be difficult to understand for readers who are new to Ferrite. But hopefully with some exercise, we should be able to get familiar with the error message and understand the meaning behind them.