Intrinsically Typed Sessions with Callbacks (Functional Pearl)

All formalizations of session types rely on linear types for soundness as session-typed communication channels must change their type at every operation. Embedded language implementations of session types follow suit. They either rely on clever typing constructions to guarantee linearity statically, or on run-time checks that approximate linearity. We present a new language-embedded implementation of session types, which is inspired by the inversion-of-control design principle. With our approach, all application programs are intrinsically session-typed and unable to break linearity by construction. Our design relies on a tiny encapsulated library, for which linearity remains a proof obligation that can be discharged once and for all when the library is built. We demonstrate that our proposed design extends to a wide range of features of session type systems: branching, recursion, multichannel and higher-order sessions, as well as context-free sessions. The multichannel extension provides an embedded implementation of session types which guarantees deadlock freedom by construction. The development reported in this paper is fully backed by type-checked Agda code.


INTRODUCTION
Session types provide a type discipline for protocols in concurrent programming systems.Each session type operator describes the direction of communication and the type of the payload (the transmitted value).Moreover, there are ways to specify control structures for protocols like sequencing, branching, and looping.
Honda and others [18,19,52] started the investigation of session types originally in the context of the pi-calculus [38].Later the concepts were adapted to other programming paradigms like functional programming [17,60] and object-oriented programming [16,46].
The early works concentrated on binary session types, i.e., protocols with two parties.Subsequently, multi-party session types were proposed [20] that relax the restriction to two parties.The Betty project resulted in several survey articles and books that provide a good overview of the field [2,4,15,24].For this paper, we mostly focus the discussion on the binary case, but draw some inspiration from multi-party session types.
Session types are inextricably connected to linear types via the Curry-Howard correspondence.This connection has been discovered and investigated in quite some depth [5,6,61] along with many ramifications [3].Moreover, this connection is not just of theoretical interest, but it has affected the implementation and (one could argue) hampered widespread adoption.Languages constructed around session types are usually special purpose languages that embrace linearity so that their type checker rejects violations thereof.Examples are plenty: Links [36], Sepi [14,59], Sill [56], C0 [62], and so on.While these languages and their implementation have fostered research and encouraged experimentation, they are not widely used.
To boost the use of session types, a lot of work has been dedicated to embedding session types in mainstream languages, most of which do not have native support for linearity.There are plenty of examples for such embeddings for functional languages like Haskell [40,45,49] and OCaml [26,43], as well as object-oriented languages like C# [29], Scala [50], Java [23].Most of these approaches ignore the issue of linearity at compile time; some ignore it entirely.Some (e.g., [43]) rely on run-time checks, others rely on encodings of linearity using lenses [25] or monads [45].There are also extension languages with a separate checker that add sessions [41] or, more generally, typestate [32] to an underlying Java program.We comment on some recent implementation that rely on Rust in the related work (Section 9).
Recent work on multi-party session types [39,64] suggests an alternative approach that does not rely on linearity.It is inspired by the design principle inversion of control which is familiar to programmers from GUI programming.The systems described in those works translate a description of a multi-party session type into a library that encapsulates the implementation of all communication.For each communication action, the library provides an interface where the programmer specifies a callback function for this particular action.
To clarify this idea, we give a very simple example, continued and extended in Section 2, in the context of a functional language.Unlike the cited work, our work as well as this example rely on binary session types.We start with the following grammar for types  and session types .The session type ! • (? •) describes a channel that is ready to send (receive) a value of payload type  and the continue as .The session type end describes a channel that can only be closed.
Traditional setting (cf.[17]) The traditional interface to session-typed communication consists of primitive operations like send : ! • ⊗  ⊸  recv : ? • ⊸ ( ⊗ ) close : end ⊸ () that send on a channel, receive from a channel, and close a channel.The crucial observation is that the type system must treat channels linearly to ensure protocol fidelity.Programs typically look like this: negp -server : ?int .! int .end ⊸ () negp -server c0 = --c0 : ?int .! int .end let (x , c1 ) = recv c0 in --c1 : !int .end let c2 = send ( c1 , -x ) in --c2 : end close c2 By linearity, the recv operation consumes c0; otherwise, another recv could be applied could c0, thus breaking the protocol.Analogous arguments apply to c1 and c2, e.g., once the channel c2 is closed, it cannot be closed again.Callback approach The callback interface to session-typed communication proposed in this work consists of two items.(1) A datatype of commands, Cmd, indexed by an application state  and a session type.This datatype constitutes an intrinsically session-typed encoding of communicating functional programs.
(2) An encapsulated interpreter exec to execute commands. 1ata Cmd (A : Set) : Session → Set where CLOSE : Cmd A end SEND : The SEND command has a callback to obtain the value to be sent from the application state.
Similarly, the RECV command has a callback to inject the received value in the application state.Both take a continuation command of type Cmd   that deals with the continuation session .The CLOSE command signifies the end of the protocol.
A program is expressed as a value of type Cmd.It looks similar to the traditional one where we choose Z, the integers, as the application state.The encoding relies strongly on the core idea of functional programming: functions (callbacks) as first class values. 2egp-command : Cmd Z (? int The least sophisticated interpreter takes a command, a suitable initial application state, an untyped channel, and results in an IO action that produces the final application state.
This interpreter is implemented once and for all in an encapsulated library.In a sense, it forms the trusted computing base of our approach, as we have the obligation to prove that it performs the commands on the channel according to the session type index of the channel.

Contributions
• We introduce the callback approach to binary session types in the context of dependentlytyped functional programming.We deploy it as a proof-of-concept specification in the language Agda, but we expect our development to be transferrable to Haskell, either via compilation or via translation [10].• Linearity of session handling is ensured by verifying linear handling of command execution in a small interpreter that forms the trusted computing base of our approach.There is no need for linear types in the type system of the host language, nor is there a need for clever type constructions to simulate linearity.• We demonstrate that the approach extends to most familiar session type constructions: branching (Section 3), recursion (Section 4), multichannel and higher-order sessions (Section 7).In Section 4.3 we offer a novel and significant improvement of the API-based treatment of recursion.• The extension to operate on multiple channels is significant and mostly orthogonal to the other features.Our approach is inspired by Wadler's GV calculus [61] and thus yields deadlock-free programs by construction.• We propose a new dynamic selection operation in the context of branching session types (Section 3).• We extend the callback approach to context-free session types (with branching and recursion), which in turn requires a more sophisticated, dependently-typed encoding of commands than regular session types (Section 6).• For monad lovers Section 5 describes a version with a monadic encoding of callbacks.
The source of this document includes a number of literate Agda scripts which will be submitted as an anonymized supplement (to be turned into an artifact).Every line of code that is typeset in color has been checked by Agda.At present, the interpreters are implemented against a small API of monadic IO operations to manipulate untyped channels.This API can be implemented in Haskell using Agda's foreign function interface. 3s a functional pearl, this paper concentrates on the library design, it contains no formal proofs of the proof obligations on the library interpreter (i.e., linearity and freedom of deadlock for the multichannel case).The discussion in Section 8 contains some suggestions how this task may be approached.
Working knowledge of Agda is not a hard requirement for understanding the paper.We strive to make the code accessible to readers who are knowledgable in Haskell by explaining features specific to Agda as they are encountered.

FINITE NON-BRANCHING SESSION TYPES
Let's start straight away with the simplest instance, finite non-branching simple session types, to convey the gist of the approach.Subsequent sections show how to add most of the usual features of session types.
A binary session type describes a bidirectional communication between two peers, let's call them server and client.The session type is attached to the type of the communication channel. 4ata Type : Set where int : Type bool : Type data Session : Set where !_•_ : Type → Session → Session ?_•_ : Type → Session → Session end : Session These Agda types correspond to the standard grammar of session types, where  is the type of payload values that can be transmitted and  is the type of sessions.
The session type ! • (? •) describes a channel that is ready to send (receive) a value of payload type  and then continue as .The session type end describes a channel that can only be closed.
Here are two examples for session types: the types of the server for a binary operation and a unary operation, respectively.
In GV, a widely studied functional session type theory [17], there are primitives to send and receive values and to close a channel with types like this: The types indicate that we must treat channel values of session type linearly: the send operation consumes a channel, which is ready to send, paired with the payload and returns it in a state described by ; the recv operation consumes a channel, which is ready to receive, and returns a pair with the received value and the updated channel; the close operation consumes the channel and returns a unit value.Enforcing this linearity is required for soundness.
In this work, we take a different approach inspired by callback programming.Instead of providing send and recv primitives to the programmer, we ask the programmer to define the "application logic" by implementing a command value whose type Cmd is indexed by a session type.This definition relies on an interpretation of types as Agda types.
In this definition, the type parameter  embodies the application state.Each SEND command takes a state transformer that extracts the value to send from the current application state; each RECV command takes a state transformer that is indexed by the received value; the CLOSE command terminates the session.In fact, we could provide the application logic by actions in a state monad over the application state .We defer the shift to a monadic interface to Section 5, when we have the full picture.
Continuing our example, we define commands that implement a server for the protocols unaryp and binaryp with the operation instantiated to negation and addition, respectively.
We use explicit lambda abstraction instead of fancy abbreviations and library combinators for clarity; the variable  stands for the application state and  and  for the respective values received from the channel.For connoisseurs of pointfree definitions, we give a more concise version of the addition command using functions from the standard library: To execute commands, we write an interpreter that relies on primitive operations provided in the IO monad.The point of our approach is that this interpreter is the single definition where we are obliged to prove that it handles channels in a linear fashion.This API should be self-explanatory. 5It declares an abstract type of untyped, raw channels with operations to accept a connection, close a channel, as well as send and receive a value over the channel.It glosses over issues like serialization, which can be addressed using type class constraints like Serialize  (in Haskell) on primSend and primRecv.
The interpreter itself is defined by induction on the type Cmd.The actual computation takes place in the IO monad and is expressed using the do notation, both familiar from Haskell.Examining the interpreter, we finally see the full monadic structure.We need a stack of monad transformers starting with a state monad for the application state on top of a reader monad providing the channel on top of the IO monad.Our proof obligation for the interpreter boils down to verifying that the interpretation of each command executes the single communication action designated by the corresponding session type operator.The correct sequencing according to the session type is imposed by the sequencing constraint underlying the IO monad.Indeed, this observation was the reason to employ monads for APIs to state-based operations in pure functional languages [28].

SELECTION AND CHOICE
Adding branching to our development is straightforward.The standard theory of session types allows branching on a finite set of labels using this syntax: Here  is a finite, non-empty set of labels, which can be chosen differently at every use of the type operator.The type constructor ⊕ corresponds to an internal choice of the program.The select primitive sends one of the labels, say ℓ ∈ , available in the type and continues according to  ℓ : The type constructor & corresponds to an external choice.The primitive branch receives one of the labels mentioned in the type and chooses a continuation according to the label.In the presence of sum types and linearity, the primitive can be typed as follows [44].
Our modeling in Agda extends the definitions of Session, Cmd, and exec from Section 2. A label set of size  is modeled by the type Fin  = {0, . . .,  − 1} and the alternative continuation sessions by functions from labels to Session (isomorphic to vectors of sessions, cf.Section 8.1).
data Session : Set where The commands for SELECT and CHOICE only differ in the placement of the parentheses: SELECT takes a label and a command corresponding to this label, whereas CHOICE takes a function that maps a label to its command.Neither command requires the application state: the selection is static and does not depend on the current state; once the external choice has been taken, the chosen command can reflect that choice in the application state.
Extending our running example, we define the type of an arithmetic server, which gives a choice between a binary operation and a unary one.This definition uses a smart constructor & for the external choice that takes a vector of session types and transforms it into the corresponding function. 6

arithp = & [ binaryp , unaryp ]
The command for the server extends in the obvious way.The vector trick does not work for the Cmd constructors SELECT and CHOICE because their arguments are dependent functions that generally return different types for different arguments.

arithp-command : Cmd
Finally, we observe that the proposed interface enables a more dynamic selection operator than in standard session types.While the standard select operator is indexed by a label that is fixed at compile time, we can supply a dynamic selector command where the label is computed by a callback getl at run time: data Cmd (A : Set) : Session → Set where DSELECT : ∀ {Si} → (getl : Extending exec to this command is straightforward.There is still room for improvement in the type of this command.We come back to this issue in Section 6.

GOING IN CIRCLES
Recursive types are a common feature of session types.They are required to model protocols for servers that perform the same functionality over and over again.Our running example will be a server that allows a client to repeatedly perform a unary operation until the client quits.
The pen-and-paper syntax of recursive types relies on type variables and a  operator like so:7 The intended semantics of the  operator is that   . is equivalent to  [ ↦ →   .], the unfolding, where we substitute the recursive type itself for the variable  in its body.
For the Agda formalization we choose the standard de Bruijn encoding of bound variables.The parameter  of the Session type denotes the number of type variables in scope.The  operator opens a new scope and '  references the th variable, the innermost binding being 0.
data Session (n : N) : Set where Further extending our running example, we redefine the protocol unaryp as a function that takes the rest of the protocol and wraps it into a recursive type.The session type many-unaryp is a recursive type that either runs a unary function and recurses or just ends the protocol.We define & as a smart constructor as in Section 3.

Commands
The Cmd type obtains a new parameter  to match the parameter of the session type used as an index.We only show the two new cases.
data Cmd (n : N) (A : Set) : Session n → Set where LOOP : With this type we are ready to implement a service that repeatedly adds numbers as they are received and sends the partial sum as a response each time.

Interpretation
Executing a command with recursion requires a new component in the executor.Whenever execution enters a LOOP command, this component saves the entire loop on a stack.When we encounter a CONTINUE  command, we grab the corresponding loop from the stack and continue with it.There are a few complications in setting up this new component that we call CmdStore.Its type requires some thought.The top-level type is a Session 0. When we enter the first loop, its type is Session 0. Hence, the CmdStore contains one entry (left column in Figure 1).Entering the next loop pushes a loop of type Session 1 (middle column).After entering the -th loop, we obtain the picture on the right of Figure 1.Here is a suitable type definition.
For some ( : Fin (suc )), the function opposite  returns  −  (as an element of the same type) and the function toN injects an element of the finite number type into the natural numbers (it is an identity function up to the type).The tricky part is implemented in the typed push and pop operations on that structure.
The push function takes a CmdStore and a suitable Cmd and returns the store extended by this command (at position zero).The pop1 function pops the first entry off the CmdStore.It gets used in defining the inductive step of the function pop, which pops any (legal) number  of entries from the stack.The definitions are simple but omitted from the text as they require invoking some technical lemmas about injections (i.e., identity functions) from Fin  to Fin (suc ).
The execution of LOOP just pushes the loop onto the CmdStore and executes the body.The case for CONTINUE  jumps to the selected loop and pops all intervening continuations from the store.It also reveals another complication.The recursive call to exec happens on a command drawn from the CmdStore.This command is unrelated to the current command, which upsets Agda's termination checker.To console it we introduce a Gas argument, which we decrement at each CONTINUE.
The question is, what to do if we run out of gas?There are at least three possibilities.
(1) We regard the current formalization as a proof of concept and refer the reader to a "real" implementation in Haskell, where the Gas argument is not required.We see no problem in creating such an implementation, but it would be more involved as dependent types are not yet fully integrated in Haskell.(2) We turn the bug into a feature and require that every CONTINUE communicates with the other end of the channel to exchange their respective gas levels and quit the loop if one end runs out of gas.This choice implies that we would have to a continuation argument to CONTINUE that acts like a finally-clause for exception and finishes the protocol in an orderly way.(3) We can modify exec so that it always takes a single step and returns either a continuation and an intermediate state or a final result of type .This modification is straightforward in the setting with loops as a value cms of type CmdStore (suc )  can serve as a continuation.
To restart a continuation, we simply invoke exec on cms zero.For concreteness, we show the continuation type and an implementation of the restart function.
With this approach, the management of restart has to be included in the trusted computing base as it also has to guarantee linear treatment of the channel.
The current formalization leaves that question open and just breaks the protocol.Our preferred solution is a Haskell implementation, which omits the gas and thus sticks to the protocol.u

What about the client?
The commands we presented so far for recursive sessions are fine for servers that repeatedly perform the same action.However, a client might want to perform different actions on each iteration.While such a behavior may be encoded in the application state, it would not be an enjoyable experience for the programmer.
Hence, we propose an UNROLL command that enables the specification of a command for one loop iteration at a time.
It specifies one command for the session type's loop body and a continuation command for the whole type to cover subsequent iterations.Executing this command means to execute its body and push its continuation on the stack.exec g (UNROLL body-cmd next-cmd) cms st ch = exec g body-cmd (push cms next-cmd) st ch As an example, we write a client for the many-unaryp protocol that iterates the protocol two times before it terminates.In each round, it sends an integer and ignores the response.

GOING MONADIC
We already remarked on the monadic structure apparent in the callbacks and in the implementation of the exec function.Indeed, moving on to a monadic interface might make our session programs more concise.We start with a slightly refactored type of commands.More precisely, we introduce a new SKIP command that only performs a state transformation via its single callback.Second, we restate the types of the callbacks for SEND and RECV in terms of the state monad.
data Cmd (A : Set) : Session → Set 2 where CLOSE : Here are the specifics of the types of the callbacks.Each callback runs in a state monad that handles the application state  on top of another monad .This construction is expressed in terms of the monad transformer StateT applied to the application state  and the underlying monad .The type of the callback does not give away more than that.But wait, using the Agda standard library, we have to state that  has a type that fits a monad and that it implements the interface RawMonad (a record that contains the basic monadic operations).Fortunately, we can abstract from these issues and adopt a Haskell-inspired syntax with a straightforward Agda definition. 8onadic : Our running examples become (even more?) concise: While we are at it, we also define the exec function in monadic style.At this point, the full type of the execution monad emerges.We already encountered the state transformer on top for handling the application state.Below we find a reader monad (again implemented using a monad transformer) that handles access to the channel.At the bottom, we have the IO monad as before.
The most remarkable aspect of this implementation is how unremarkable it is.However, bear in mind that monads have been used for a long time to contain linear resources, most notably with the inception of monadic IO [28].That is, our proof obligation of linear handling for the channel becomes even simpler in this monadic setting.
We conclude this section with the definition of the monadic acceptor.It essentially invokes the runners of the reader and state monads and executes the underlying IO action.Context-free session types [1,44,54] have been conceived to liberate session types from the restriction to tail recursion.Alleviating this restriction makes session-typed programming more compositional and enables low-level programming tasks like the serialization of tree structures.The basic idea [54] is to reorganize the type language of session types as follows.
Now ! (? ) describes just the act of sending (receiving) a value of type  .To combine two session types, we have to use sequential composition   with unit skip.The branching types and recursion are as before, so we do not repeat them here.The Agda encoding of this structure combines straightforwardly with the accumulated work of the previous sections.The revised command structure has a few catches that require explanation.

LOOP : Cmd
The Cmd datatype now carries four type-related parameters on top of the session type.A command of type Cmd      is firstly an action that takes an input of type , yields an output of type , and executes a session according to .The additional parameters,  and  , are vectors of types that control the typing of the currently pending loops, which are explained with the commands LOOP and CONTINUE.For now, we can think of them as stack of the input and output types of those pending loops.
The SKIP command is associated with a skip in the session type.It comes with a function that transforms s into s.
The SEND and RECV commands no longer take a command parameter to process the continuation session.This functionality is now provided once and for all by the composition operator.
The command SELECT prescribes dynamic selection.It takes a callback that maps a value of input type  to a dependent pair of a label  and a value of type  . 9 The continuation takes advantage of this fine-grained type information by associating the label  with the input type   as produced by the callback.
The CHOICE command is as before up to the additional type parameters.The composition operator for two commands has three additional callback arguments, which we call split, cross, and join.The first callback split splits the input type into one part that gets consumed by the first command and another that bypasses it.The second callback cross joins the output type and the bypass into the input type for the second command.The third callback join combines the outputs of the two commands.
In the context-free case, the behavior of the LOOP command is more general than the simple tail-recursive iteration in Section 4. It takes a loop body that transforms s into s with these same types pushed on the stacks.
The CONTINUE  command invokes the th pending loop.To do so, the current type must match the typing of the loop, which we find by lookup up the input and output type on the stacks at position .

Examples
Before we delve into the interpreter, let's take stock what we achieved by reviewing our old examples as well as a new one that demonstrates the additional expressivity of context-free sessions.
-service protocol for a binary function binaryp : Session n binaryp = ?int ?int !int -service protocol for a unary function unaryp : Session n unaryp = ?int !int -service protocol for choosing between a binary and a unary function arithp : Session n arithp = & [ binaryp , unaryp ] -many unary functions 9 The negative occurrence of the  parameter is not permitted in the constructor of a Set datatype.Its presence forces the command type one level up into Set

many-unaryp : Session n many-unaryp = 𝜇 (& [ unaryp ' zero , skip ])
Compare these types with the corresponding ones from Section 4, where the protocol fragments are metafunctions that take a continuation session as a parameter.No such parameterization is needed with context-free session types.They are intrinsically compositional and modular.
The final example many-unaryp contains a tail recursive part.The second component of the choice must make use of the skip type, because the arm of a choice cannot be empty.
Let's consider servers that implement those protocols.
The code of the servers does not look that different from before.However, unlike before, each server is now reusable as part of the implementation of a larger protocol.One indication is the use of the parameters  and  in the types, another is the lack of the CLOSE constructor which prescribes closing the connection.
As these protocols are still tail-recursive, their implementation uses a binary composition operator for commands that is tailored for this use case.The split function feeds everything to the first command; the cross operation ignores the bypassed value, passing only the output of the first command to the second; and the join operation passes only the output of the second command.
To further pinpoint the difference between context-free sessions and regular session we finally present a protocol that exploits the power of context-free session types by going beyond tail recursion: servers that receive and send binary trees.The session types leafp and branchp encode receiving a leaf and a branch of the binary tree type IntTree.The session type treep provides the enclosing recursion and choice between the leaf and branch protocols.As branchp contains two recursive calls, the protocol treep is no longer tail recursive.
The receiver for the leaf (choice zero) wraps the received value in the Leaf constructor and returns it.The receiver for the branch (choice suc zero) obtains the left and right subtrees by recursive calls CONTINUE zero and combines the outcomes using the Branch constructor.The split and cross callbacks just manipulate values of unit type.The sender of a tree makes full use of the callbacks for command composition.
The function splitTree implements essentially (one half of) the isomorphism that unfolds the recursion in type IntTree; function IntTreeF helps construct its type.It is straightforward to construct the sender of a tree using these helpers.Using splitTree as the callback for SELECT, we find that the zero branch is invoked with an integer and the suc zero branch is invoked with a pair of the two subtrees.The split callback is just the identity, so that the left subtree is passed to the first recursive call and the right one to the second.

Interpretation
We show the full exec function and finish with a rationale why the callback interface in this section is not monadic.
The implementation of SKIP just executes the action.Sending SEND, receiving RECV, and choice CHOICE are as usual.The dynamic selection is improved with respect to Section 3. Previously, there was no connection between the selected label and the executed continuation, so that a bug in the interpreter might introduce a mismatch.In the present interpreter, no such mismatch is possible because it would be a type error to invoke any other continuation than cont  with .
Composition first splits the input using the split callback.It performs the left command, obtains its final state in  1 , combines it with the bypass value  ′ using cross, and then performs the right command.It obtains its final state in  2 and returns join  1  2 .The implementations for LOOP and CONTINUE are as before in Section 4. The difference is that the CONTINUE operation may now appear in the context of a composition which provides a nontrivial continuation.In Section 4, the function exec is tail recursive, but here it is not!Now we turn to the question of the monadic callback interface.Examination of the code reveals that the state monad is no longer the most appropriate model of a callback.On first sight, the type  →   is the same as ReaderT   .However, the case for composition shows that a reader does not fit because the type  of the reader's source changes between recursive calls to the interpreter.Moreover, the callbacks for composition do not fit the pattern of the reader monad at all.Thus, we refrain from the monadic interface.

HANDLING MULTIPLE CHANNELS
We have to amend some final elements to fully encompass traditional binary session types: a thread can open and manipulate many channels at a time and channels can be delegated.So far, our interfaces were restricted to single channels and to transmitting pure data (i.e., no channels).We now turn to lifting these restrictions.
To concentrate on the new issues, we start out by slightly rephrasing session types as we know them from Section 3. In particular, we restrict to binary branching and leave the extension to finitary branching as well as the addition of recursion as an exercise to the reader.Generally, these types describe the communication on a single channel, as before.There are two novel aspects: we factorize the specification of the direction of communication and we add a special type for channel delegation, i.e., sending or receiving a channel.An actual multichannel session type describes the interleaved communication on all channels at once.It comes with features and restrictions very similar to multiparty session types [20].
-assume new channel has address zero in both threads delegateOUT : (c j : Fin (suc n)) → c j → Session → MSession n → MSession (suc n) delegateIN : (c : Fin n) → MSession (suc n) → MSession n -received channel has address zero in continuation A multichannel session type MSession n, ranged over by , is indexed by the number  of channels that it governs.Multichannel session types are loosely based on Wadler's GV calculus [61].In particular, the connection topology of multichannel programs is restricted in the same way as in Wadler's GV, with the pleasing consequence that they are guaranteed to be free of deadlocks.
The names of the channels are represented by de Bruijn indices.Unlike in other embedded implementations, a multichannel type covers the full choreography of a communicating application: connecting (forking) to a new process is combined with channel creation, branching and transmitting values as usual, closing a channel, delegation, and terminating the application after all protocols are finished and their respective channels closed.
Each multichannel type, except connect and terminate, takes a parameter  that identifies the channel on which the command operates.The type to transmit a value (transmit) is as before, save the direction and channel parameters.Branching (branch) is as before except the extra function Causality: it ensures that a branch is reasonable by enforcing that the session on every channel except  is not affected by the branch.This restriction is called "Causality" in the context of multiparty session types.It is needed because only the party on other end of channel  knows about the branching, but the others do not.Given the preceding discussion as well as the discussion in previous sections, the types of commands follow directly.
data Cmd (A : Set) : (n : N) → MSession n → Set 1 where CLOSE : Likewise, the interpreter exec is an easy exercise.The reader may wonder why we do not define the constructors for selection and choice using vectors of length , rather than functions from Fin  → Session (which is isomorphic).Using a function extends directly to the definition of the Cmd type where the Session index of the continuation command depends on the function argument  : Fin .We would have to define a special dependent vector type to achieve similar expressivity.

Alternative representations
Instead of interpreting a command value, we could compile it to a custom library, following the lead of the related work [39,64].Such a compiler can be obtained specializing the command interpreter with respect to single commands.The resulting denotational implementation corresponds to a library implementation that exposes the commands, which are reified in a datatype in our approach, as functions.This approach has been pioneered by Reynolds [48] and subsequently applied, e.g., in the context of partial evaluation and program analysis [7,53].
Here is an excerpt of such an implementation derived from the interpreter in Section 2.
The interpreters from other sections can be rephrased analogously without sacrificing the advantages our approach.However, for program development the command-based approach is more attractive because the Agda interactive programming support features autocompletion for commands, but not for combinators.

Multiparty session types
We see no issues in extending the approach presented in this work to protocols with more than two participants.We refrained from doing so in this work to avoid the extra complication.Section 7 already gives some insight into the requirements for a full multiparty version.

Verification
A fly in the ointment of our approach is the proof obligation that the interpreter does not break linearity.As the interpreters are very simple, it is tempting to rely on manual inspection.To obtain a formal and possible mechanized proof, we envision a semantics in terms of a free monad [30,51] with uninterpreted occurrences of the operations of our primitive channel IO API.The actual proof might be conducted using interaction trees [63], a mechanized framework for representing about recursive and impure programs and reasoning about them.

RELATED WORK
In this section, we review how different library implementations of session types deal with linearity.Specifically, we do not consider dedicated language implementations like SILL [56], SEPI [14], FreeST [1], or Links [36].These implementations come with dedicated type checker that properly treat linearity at compile time.
There are libraries with fully dynamic enforcement of a session type discipline [21,37].They suffer from run-time overhead as they have to check every communication operation and they lack guarantees as they terminate the protocol when an error is detected at some peer.
Several libraries perform linearity checks at run time.While a check for at most one (affine) use of a resource can be performed with a single bit [57], checks for linearity are more expensive, but still deemed lightweight [22].
Several libraries statically enforce linearity by encoding it using parameterized monads [45,49], polymorphism [26], or higher-order abstract syntax [35].While these encodings cleverly exploit the facilities of the host language and support type inference, they are nontrivial to explain and yield types that are not easy on the programmer.No such cleverness is needed in our approach; types are human-readable and the interactive Agda system helps with constructing types and programs, but session types are not inferred.
None of the existing embeddings offers a feature like our dynamic selection.However, dynamic selection can be viewed as a special case of label-dependent session types [55], so that our approach implements part of that theory, too.
While the dynamic and object-oriented libraries support session subtyping, our approach currently does not support subtyping.
The most closely related work is by Miu, Ferreira, Yoshida, and Zhou [39].They develop an implementation of multiparty session types in TypeScript by generation of custom libraries from a protocol specification.Their implementation guarantees freedom from communication errors, including deadlocks, communication mismatches, channel usage violation or cancellation errors.They generate TypeScript APIs in callback style, where the finite state machine underlying the communication endpoints is reified in terms of interfaces.Sending and receiving are both encoded via callbacks into the library or into the user program, as appropriate.The generated APIs encapsulate all primitive communication operations.

Haskell
The earliest Haskell implementation by Neubauer [40] emphasizes the modeling of the type of a single session using phantom types.Duality is implemented using type classes with functional dependencies.Linearity is not considered.
Sackman [49] and Tov [58] model multiple channels using a parameterized monad that is indexed by a mapping from channel names to their current session types.The mapping changes with each operation to keep track of the current state of all channels.The monadic interface guarantees linearity.
This approach leaves more freedom to the programmer than our proposal in Section 7.While individual channel types are independent in their approach, our multi-channel session types impose a choreography on all connections, which is closer in spirit to multiparty session types.
Lindley and Morris [35] extend a HOAS encoding of linear lambda calculus with monads and session primitives in the style of GV [17].They call their approach "parameterized tagless" [7], which means that they encode the syntax of lambda calculus and the session primitives in term of parameterized functions in a type class with implementations provided subsequently.
Orchard and Yoshida [42] discuss connections between session types and effect systems.They present an implementation of session types in Haskell via an effect system encoding based on graded monads.Here the "grading" keeps track of the currently active channels and their session types whereas the monadic structure provides proper sequencing.

OCaml
The implementation of FuSe [43] consists of a typed layer on top of untyped channels (just like our approach).It provides an API inspired by GV, supports type inference, and checks linearity at run time.The approach is extensible to context-free session types [44] at the expense of some user annotations (while keeping type inference).
Session OCaml [26] enforces linearity by parametric polymorphism based on a technique by Garrigue 11 with significant extensions to deal with session types.It can handle a fixed number of channels at the same time with globally defined channel names (slot names).The technique hinges on the polymorphic types of the global slot names.Session types are inferred from programs that can be written in a notation similar to FuSe and GV.

Java
The early interface proposed in "Session-based distributed programming in Java" [23] relied on special syntax for session communication and a preprocessor to limit aliasing.Mungo [32] and Bica [16] use similar ideas to implement typestate and sessions.
Hu and Yoshida [22] pioneered code generating approaches for implementing multiparty session types.They generate protocol-specific endpoint APIs from multiparty session types for Java, but claim generality of their approach for mainstream languages.They start from the observation that the behavior of an endpoint of a communication can be represented by a finite state machine.Each of these states is reified as a state channel type with methods that imply communication operations corresponding to state transitions.As Java places no restrictions on object uses, they deploy "very light run-time checks in the generated API that enforce a linear usage discipline on instances of the channel types." Their abstract I/O state interfaces are closest to the facilities that we provide.
While their approach reifies the different possible states of a communication endpoint, these states are implicit in our approach.Moreover, while an API based on finite state machines is entirely appropriate for servers, we can provide additional flexibility for implementing clients of recursive protocols though commands like UNROLL described in Section 4.3.
Scalas work for Scala [50] follows similar ideas with run-time checks for linearity.More recent work [9] improves on the flexibility by relying on advanced typing features in Scala 3.

Rust
Several recent implementations of session types [8,11,12,27,31,33,34] rely on Rust, a language with uniqueness types and ownership, all checked at compile time.While uniqueness types are quite similar to affine types (describing values that can be used at most once), they do not quite get to the level needed for sessions: having an affine session type for a channel means that an agent can drop the connection anytime without finishing the protocol, which leads to deadlock at the other end of the connection.With a proper linear session type, every agent has to fulfil the protocol up to the closing of the connection.

CONCLUSIONS
Our paper demonstrates that a callback-based approach to implementation session-typed programs is a perfect fit with (mildly) dependently-typed functional programming.The intrinsically sessiontyped way of writing program guarantees protocol fidelity and, in some instances, deadlock freedom by construction.In connection with the callback approach, the host language does not have to support linearity, so that programs are statically safe, once the encapsulated library is verified.
Interestingly, the changed angle of attack revealed the possibility of and the need for two novel constructions, the dynamic selection and the UNROLL command for recursive sessions.The first one is facilitated by our dependently-typed host language.The second one arose from the need to write client programs for recursive protocols.
The discussion in Section 8 contains several pointers for future work.It would also be interesting to investigate ways to integrate subtyping (an obvious one being the insistence on explicit coercions).

record
Accepting A s : Set 2 where constructor ACC field pgm : Cmd A s acceptor : Accepting A s → A → IO A acceptor (ACC pgm) a = do ch ← primAccept 〈 final , _ 〉 ← runReaderT (runStateT (exec pgm) a) ch pure final 6 CONTEXT-FREE SESSION TYPES