Sharing is caring
Table of contents
You’ve come far in FreeST, you’ve used channels in many different ways but you ask yourself “Can I connect more than two processes with linear channels?”, and the answer is a reluctant “Yes, but…”. Linear channels are expressive but can only connect two participants.
One way to implement one-to-many, many-to-one or even many-to-many communication is to use lists of channel ends and iterate through them as needed. This solution works only when the number of clients and servers is fixed and known à priori. But even in this case the solution looks simpler than it is. Reading on a FreeST’s channel is a blocking operation, and thus, reading values from a list of channels may become a slow sequential operation if writers are not ready.
To fulfil the use-cases left open by linear channels, FreeST offers unrestricted, or shared, channels. Shared channels are simpler in their types but they can be copied and used (or not) by as many processes as needed. The most important aspect of shared channels for you to remember is shared channels are stateless, they offer the same behaviour forever.
We can express a shared integer stream from the point of view of the reader as *?Int
. Many processes can receive
from this channel as long as there are processes on the other side available to send
integer values.
There are four shared session types at your disposal:
*?T
*!T
*+{l, ..}
*&{l, ..}
Why do choices have no continuation? You might ask. Because shared session types offer the same behaviour forever, and the choice by itself is the behaviour offered. There cannot be a continuation different from the choice itself.
Reading and Writing on Shared Channels
Let us a consider the type of a stream of integer values as seen from the producer side.
type IntStream = *!Int
To produce an unbounded number of integers n
on an IntStream
channel end one may write:
produce : Int -> IntStream -> Diverge
produce n p = p |> send n |> produce n
Type Diverge
is a mark for functions that do not terminate; it is defined as ()
.
To consume a stream and print its values we may write:
consume : dualof IntStream -> Diverge
consume c =
let (x, c') = receive c in
print @Int x;
consume c'
In all cases (linear or unrestricted) variables c
and c'
denote the same channel end. When c
is of a linear type the type of c'
may differ from that of c
. But unrestricted types offer an uniform behaviour, so that one may write instead:
consume : dualof IntStream -> Diverge
consume c =
let (x, _) = receive c in
print @Int x;
consume c
In order to simplify reading from shared channels, FreeST offers function receive_
that discards the channel from the pair and returns the value read. One can then write:
consume : dualof IntStream -> Diverge
consume c =
c |> receive_ @Int |> print @Int ;
consume c
We can then write a simple program that consumes and prints numbers from two producers:
main : Diverge
main =
let c = forkWith @IntStream @() consume in
fork (\_: () -> produce 1 c);
produce 2 c
Discarding Shared Channels
Linear channels cannot be discarded; they must be used until the end, that is Close
or Wait
depending on the point of view (if they terminate at all). Shared channels on the other hand may be discarded at any time. This program creates a channel that it never uses.
main : (IntStream, dualof IntStream)
main =
new @IntStream ()
Notice that shared channels can never be closed even if they are not used.
A more interesting example consumes a finite number of values from an integer stream. The result is the sum of the numbers read.
consume : Int -> dualof IntStream -> Int
consume n c =
if n == 0
then 0
else receive_ @Int c + consume (n - 1) c
The main
function is now composed of two infinite producers and one finite consumer.
main : Int
main =
let (p, c) = new @IntStream () in
fork (\_: () 1-> produce 1 p);
fork (\_: () 1-> produce 2 p);
consume 5 c
We have also rearranged the code in such a way that consume
runs on the main thread, so that the program may terminate once the five values are read. The program should print a number between 5
and 10
.
Session initiation
If shared channels are more practical than linear ones, why bother with linear channels? Well, you can’t serialize a tree directly on a shared channel for example. Shared channels lack the capacity of expressing interesting protocols.
The trick with shared channels is to use them not as a single communication point, but as a rendez-vous point to establish more complex communication in a linear channel by exchanging endpoints. We call this process session initiation.
Let’s think of a very useful case in programming, a shared data structure, in this case a single memory cell.
type Cell = +{ Read: ?Int
, Write: !Int
}; Close
The Cell
session type describes our possible interaction with an Int
memory cell.
type SharedCell = *?Cell
The SharedCell
type describes a channel on which a client can receive
another channel with the Cell
type. An example of a client that writes to the cell is:
cellClient : SharedCell -> ()
cellClient c =
c |> receive_ @Cell -- acquire a linear channel
|> select Write -- interact on the linear channel
|> send 5
|> close
However, it is the server that bears the important part of session initiation: to create the new channel and send one of its ends. To aid session initiation, FreeST offers the accept
function which creates the channel endpoints, sends one to the client and returns the other. Taking advantage of pattern-matching on external choice types, we may thus write:
cellServer : dualof SharedCell -> ()
cellServer c = serve $ accept @Cell c
serve : dualof Cell -> ()
serve (Read s) = s |> send 0 |> wait
serve (Write s) = receiveAndWait @Int s ; ()
To use a single function we may use the match-with
destructor for external choice types. The match returns the continuation channel, so that we may wait
for the other side to close the channel.
cellServer : dualof SharedCell -> ()
cellServer c =
match accept @Cell c with {
Read s ->
send 0 s,
Write s ->
let (i, c) = receive s in c
} |> wait
We usually initiate sessions at the server side for we usually have more clients that servers and hence spare all clients from the process of creating and distributing channels. Nevertheless sessions may easily be started at the client side if needed.
The accept
function can be easily written with new
and send
as follows:
accept : *!a -> dualof a
accept ch =
let (x, y) = new @a () in
send x ch;
y