packages feed

simple-sessions-0.1: Control/Concurrent/SimpleSession/SessionTypes.lhs

\section{Session Types in Haskell}
\label{sec:session}

\ignore{

> {-# LANGUAGE TypeOperators,
>              EmptyDataDecls,
>              MultiParamTypeClasses,
>              FunctionalDependencies,
>              UndecidableInstances #-}
> module Control.Concurrent.SimpleSession.SessionTypes (
>   Z, S,
>   Eps, (:!:), (:?:), (:+:), (:&:), Rec, Var,
>   Dual
> ) where
> 
> infixr 3 :!:, :?:
> infix 2 :+:, :&:
>
> data Z
> data S n

}

The central idea of session types \citep{r:gay99} is to parameterize a
channel with some type that represents a protocol, which the type system
then enforces.  In Haskell, we may encode a protocol using ordinary
datatypes:

> data (:!:) a r
> data (:?:) a r
> data Eps

These datatypes require no constructors because they will have no
run-time representation.

If |a| is any type, and |r| is a protocol, then
we interpret |a :!: r| as the protocol, ``first send an |a|, and
then continue with |r|.''  Similarly, we interpret |a :?: r| as
the protocol, ``receive
an |a|, and then continue with |r|.''  The type |Eps| represents the empty
protocol of a depleted channel that is not yet closed.

For example, the type |Int :!: Bool :?: Eps| represents the protocol,
``send an |Int|, receive a |Bool|, and close the
channel.''\thinspace\footnote{The type constructors |(:!:)| and
|(:?:)| are declared right associative and with higher precedence than
|(:+:)| and |(:&:)|.}

If the process on one end of a channel speaks a particular protocol,
its correspondant at the other end of the channel must be prepared to
understand it.  For example, if one process speaks |Int :!: Bool :?: Eps|,
the other process must implement the dual protocol
|Int :?: Bool :!: Eps|.  We encode the duality relation using a type
class with multiple parameters and functional dependencies
\citep{Jones1997Type,Jones2000Type}.

> class Dual r s | r -> s, s -> r

The functional dependencies indicate that duality
is bijective, which helps Haskell to infer
protocols and enables a form of subtyping.  Sending and receiving are
dual:  if |r| is dual to |s|, then |a :!: r| is dual to |a :?: s|.  The
empty session is dual to itself.

> instance Dual r s => Dual (a :!: r) (a :?: s)
> instance Dual r s => Dual (a :?: r) (a :!: s)
> instance Dual Eps Eps

\par
Our session types also represent alternation and recursion.  If |r|
and |s| are protocols, then |r :+: s| represents an active choice
between following |r| or |s|.  The type |r :&: s| represents
an offer to follow either |r| or |s|, as chosen by the other
process.

> data (:+:) r s
> data (:&:) r s

The two alternation operators are dual:

> instance (Dual r1 s1, Dual r2 s2) =>
>                    Dual (r1 :+: r2) (s1 :&: s2)
> instance (Dual r1 s1, Dual r2 s2) =>
>                    Dual (r1 :&: r2) (s1 :+: s2)

\par
Recursion turns out to be slightly more difficult.  It is tempting to
use a fixed-point combinator, but this would require constructing a
type of kind $\star \to \star$ for any desired loop body, which
is not generally possible.  We need
some other way for a recursive type to refer to itself, so we represent
this binding using de~Bruijn indices.

> data Rec r
> data Var v
> 
> instance Dual r s => Dual (Rec r) (Rec s)
> instance Dual (Var v) (Var v)

The type |Rec r| adds a binding for |r| inside |r|; that is, it
implicitly defines a variable bound to the whole of |r| that can be used
\emph{within} |r|.  We use |Var v| to refer to the variable bound by
the |v|th |Rec|, counting outward, where |v| is a Peano numeral
written with type constructors |Z| and |S|
(\emph{e.g.,} |Z| or |S (S Z)|).  For example, the protocol

< Request :!: Rec (Response :?: (Var Z :&: Eps))

says to send a request and then be prepared to receive one or more
responses.  By contrast, a process implementing the protocol

< Request :!: Rec ((Response :?: Var Z) :&: Eps)

must send a request and be prepared to accept any number of responses.