Safe Haskell | None |
---|---|
Language | GHC2021 |
Extensions |
|
Network.TypedProtocol.Core
Description
This module defines the core of the typed protocol framework.
Synopsis
- class Protocol ps where
- data Message ps (st :: ps) (st' :: ps)
- type StateAgency (st :: ps) :: Agency
- type StateToken :: ps -> Type
- class StateTokenI (st :: ps) where
- stateToken :: StateToken st
- data PeerRole
- data SingPeerRole (pr :: PeerRole) where
- data Agency where
- data SingAgency (a :: Agency) where
- data RelativeAgency where
- type family Relative (pr :: PeerRole) (a :: Agency) :: RelativeAgency where ...
- data ReflRelativeAgency (a :: Agency) (r :: RelativeAgency) (r' :: RelativeAgency) where
- ReflClientAgency :: forall (r :: RelativeAgency). ReflRelativeAgency 'ClientAgency r r
- ReflServerAgency :: forall (r :: RelativeAgency). ReflRelativeAgency 'ServerAgency r r
- ReflNobodyAgency :: forall (r :: RelativeAgency). ReflRelativeAgency 'NobodyAgency r r
- type WeHaveAgencyProof (pr :: PeerRole) (st :: ps) = ReflRelativeAgency (StateAgency st) 'WeHaveAgency (Relative pr (StateAgency st))
- type TheyHaveAgencyProof (pr :: PeerRole) (st :: ps) = ReflRelativeAgency (StateAgency st) 'TheyHaveAgency (Relative pr (StateAgency st))
- type NobodyHasAgencyProof (pr :: PeerRole) (st :: ps) = ReflRelativeAgency (StateAgency st) 'NobodyHasAgency (Relative pr (StateAgency st))
- type family FlipAgency (pr :: PeerRole) :: PeerRole where ...
- class IsActiveState (st :: ps) (agency :: Agency) where
- activeAgency :: ActiveAgency' st agency
- type ActiveState (st :: ps) = IsActiveState st (StateAgency st)
- notActiveState :: forall ps (st :: ps). (StateAgency st ~ 'NobodyAgency, ActiveState st) => StateToken st -> forall a. a
- type ActiveAgency (st :: ps) = ActiveAgency' st (StateAgency st)
- data ActiveAgency' (st :: ps) (agency :: Agency) where
- ClientHasAgency :: forall {ps} (st :: ps). StateAgency st ~ 'ClientAgency => ActiveAgency' st 'ClientAgency
- ServerHasAgency :: forall {ps} (st :: ps). StateAgency st ~ 'ServerAgency => ActiveAgency' st 'ServerAgency
- data IsPipelined where
- Pipelined :: N -> Type -> IsPipelined
- NonPipelined :: IsPipelined
- type family Outstanding (pl :: IsPipelined) :: N where ...
- data N
- data Nat (n :: N) where
- natToInt :: forall (n :: N). Nat n -> Int
- unsafeIntToNat :: forall (n :: N). Int -> Nat n
Introduction
A typed protocol between two peers is defined via a state machine: a collection of protocol states and protocol messages which are transitions between those states.
Start from the idea that a protocol is some language of messages sent between two peers. To specify a protocol is to describe what possible sequences of messages are valid. One simple but still relatively expressive way to do this is via a state machine: starting from some initial state, all possible paths through the state machine gives the set of valid protocol traces. This then dictates what a peer participating in a protocol may produce and what it must accept.
In this style we have a fixed number of states and in each state there is some number of valid messages that move us on to the next state. This can be illustrated as a graph, which can be a helpful form of documentation.
We further constrain this idea by saying that the two peers will use the same state machine and change states in lock-step by sending/receiving messages. In this approach, for each protocol state, the description dictates which peer has the agency to choose to send a message, while correspondingly the other must be prepared to receive the message.
The views of the two peers are dual. In each state one peer can send any message that is valid for the current protocol state while the other must be prepared to receive any valid message for current protocol state.
We can also have terminal protocol states in which neither peer has agency.
So part of the protocol description is to label each protocol state with the peer that has the agency in that state, or none for terminal states. We use the labels "client" and "server" for the two peers, but they are in fact symmetric.
Defining protocols
The Protocol
type class bundles up all the requirements for a typed
protocol, which are in fact all type level constructs. Defining a new
protocol and making it an instance of the Protocol
class requires the
following language extensions:
{-# LANGUAGE GADTs, TypeFamilies, DataKinds #-}
The type class itself is indexed on a protocol "tag" type. This type does double duty as the kind of the types of the protocol states.
We will use as a running example a simple "ping/pong" protocol. (You can see the example in full in Network.TypedProtocol.PingPong.Type.) In this example protocol the client sends a ping message and the serve must respond with a pong message. The client can also terminate the protocol. So modelled as a state machine this protocol has three states, the one in which the client can send a ping or terminate message, the one in which the server must send a pong, and the terminal state where neither can send anything. We somewhat arbitrarily choose label these protocol states as "idle" "busy" and "done".
For this ping pong example the protocol tag and the protocol state types would be defined (via promoted data kinds) as:
data PingPong where StIdle :: PingPong StBusy :: PingPong StDone :: PingPong
We use DataKinds
promotion here so StIdle
, StBusy
and StDone
are
types (of kind PingPong
) representing the three states in this
protocol's state machine. PingPong
itself is both the kind of these types
and is also the tag for protocol. We only ever use these as types, via the
DataKinds
promotion, never as value level data constructors.
Having defined our protocol tag and states we can instantiate the Protocol
type class and fill out the other details.
The protocol must define what its messages are. These form the state transitions in the protocol state machine. Each transition specifies a "from" and "to" state as type parameters. This of course determines in which protocol states each message can appear.
In the "ping/pong" protocol example, the messages are of course ping and pong, which transition between the two main states. There is also a done message that moves the system into a terminal state.
instance Protocol PingPong where data Message PingPong from to where MsgPing :: Message PingPong StIdle StBusy MsgPong :: Message PingPong StBusy StIdle MsgDone :: Message PingPong StIdle StDone
This says that in the idle state a ping message takes us to the busy state, while a pong message takes us back to idle. Also in the idle state a done message takes us to the done state.
It is not required that protocols have any terminal states or corresponding transitions, as in this example, but it is often useful and it aids testing to have protocols that terminate cleanly as it allows them to return a result.
As described above, this style of protocol gives agency to only one peer at once. That is, in each protocol state, one peer has agency (the ability to send) and the other does not (it must only receive).
In the "ping/pong" protocol example, the idle state is the one in which the client can send a message, and the busy state is the one in which the server must respond. Finally in the done state, neither peer can send any further messages. This arrangement is defined as so:
-- still within the instance Protocol PingPong type StateAgency StIdle = ClientAgency type StateAgency StBusy = ServerAgency type StateAgency StDone = NobodyAgency
In this simple protocol there is exactly one state in each category, but in general for non-trivial protocols there may be several protocol states in each category.
Finally we need to point which singletons to use for the protocol states
-- still within the instance Protocol PingPong, 'SPingPong' type is what we define next. type StateToken = SPingPong
Furthermore we use singletons to provide term level reflection of type level
states. One is required to provide singletons for all types of kind
PingPong
. These definitions are provided outside of the Protocol
type
class. This is as simple as providing a GADT:
data SingPingPong (st :: PingPong) where SingIdle :: SingPingPong StIdle SingBusy :: SingPingPong StBusy SingDone :: SingPingPong StDone
together with StateTokenI
instance (similar to SingI
from the
"singletons" package):
instance StateTokenI StIdle where stateToken = SingIdle instance StateTokenI StBusy where stateToken = SingBusy instance StateTokenI StDone where stateToken = SingDone
This and other example protocols are provided in "typed-protocols-examples" package.
The protocol type class bundles up all the requirements for a typed protocol.
Each protocol consists of four components:
- the protocol itself, which is also expected to be the kind of the types of the protocol states. The class is indexed on the protocol itself;
- the protocol messages;
- a type level map from the protocol states to agency: in each state either client or server or nobody has the agency.
- a singleton type for the protocol states (e.g.
StateToken
type family instance), together withStateTokenI
instances.
It is required provide StateToken
type family instance as well as
StateTokenI
instances for all protocol states. These singletons allow one
to pattern match on the state, which is useful when defining codecs, or
providing informative error messages, however they are not necessary for
proving correctness of the protocol. These type families are similar to
Sing
and SingI
in the "singletons" package.
Associated Types
data Message ps (st :: ps) (st' :: ps) Source #
The messages for this protocol. It is expected to be a GADT that is
indexed by the from
and to
protocol states. That is the protocol state
the message transitions from, and the protocol state it transitions into.
These are the edges of the protocol state transition system.
type StateAgency (st :: ps) :: Agency Source #
Associate an Agency
for each state.
type StateToken :: ps -> Type Source #
A type family for protocol state token, e.g. term level representation of type level state (also known as singleton).
This type family is similar to Sing
type class in the "singletons"
package, but specific for protocol states.
class StateTokenI (st :: ps) where Source #
A type class which hides a state token / singleton inside a class dictionary.
This is similar to the SingI
instance, but specific to protocol state
singletons.
Methods
stateToken :: StateToken st Source #
The connect
proof rely on lemmas about the
protocol. Specifically they rely on the property that each protocol state is
labelled with the agency of one peer or the other, or neither, but never
both. This property is true by construction, since we use a type family
StateAgency
which maps states to agencies, however we still need an evince
that cases where both peer have the agency or neither of them has it can be
eliminated.
The packages defines lemmas (in a hidden module) which are structured as
proofs by contradiction, e.g. stating "if the client and the server have
agency for this state then it leads to contradiction". Contradiction is
represented as the Void
type that has no values except ⊥.
The framework defines protocol-agnostic proofs (in the hidden module
Lemmas
) which excludes that the client and server
have agency at the same time.
exclusionLemma_ClientAndServerHaveAgency
,terminationLemma_1
,terminationLemma_2
.
Engaging in protocols
PeerRole
Types for client and server peer roles. As protocol can be viewed from either client or server side.
Note that technically "client" and "server" are arbitrary labels. The framework is completely symmetric between the two peers.
This definition is only used as promoted types and kinds, never as values.
data SingPeerRole (pr :: PeerRole) where Source #
Constructors
SingAsClient :: SingPeerRole 'AsClient | |
SingAsServer :: SingPeerRole 'AsServer |
Instances
Show (SingPeerRole pr) Source # | |
Defined in Network.TypedProtocol.Core Methods showsPrec :: Int -> SingPeerRole pr -> ShowS # show :: SingPeerRole pr -> String # showList :: [SingPeerRole pr] -> ShowS # |
Agency and its evidence
The protocols we consider either give agency to one side (one side can send a message) or the protocol terminated. Agency is a (type-level) function of the protocol state, and thus uniquely determined by it.
The following types define the necessary type-level machinery and its
term-level evidence to provide type-safe API for `typed-protocols`.
Required proofs are hidden in an (unexposed) module
Network.TypedProtocol.Lemmas
.
A promoted data type which denotes three possible agencies a protocol state might be assigned.
Constructors
ClientAgency :: Agency | The client has agency. |
ServerAgency :: Agency | The server has agency. |
NobodyAgency :: Agency | Nobody has agency, terminal state. |
Instances
SingI 'ClientAgency Source # | |
Defined in Network.TypedProtocol.Core Methods sing :: Sing 'ClientAgency # | |
SingI 'NobodyAgency Source # | |
Defined in Network.TypedProtocol.Core Methods sing :: Sing 'NobodyAgency # | |
SingI 'ServerAgency Source # | |
Defined in Network.TypedProtocol.Core Methods sing :: Sing 'ServerAgency # | |
type Sing Source # | |
Defined in Network.TypedProtocol.Core |
data SingAgency (a :: Agency) where Source #
Constructors
SingClientAgency :: SingAgency 'ClientAgency | |
SingServerAgency :: SingAgency 'ServerAgency | |
SingNobodyAgency :: SingAgency 'NobodyAgency |
Instances
Show (SingAgency a) Source # | |
Defined in Network.TypedProtocol.Core Methods showsPrec :: Int -> SingAgency a -> ShowS # show :: SingAgency a -> String # showList :: [SingAgency a] -> ShowS # |
data RelativeAgency where Source #
A promoted data type which indicates the effective agency (which is
relative to current role). It is computed by Relative
type family.
Constructors
WeHaveAgency :: RelativeAgency | |
TheyHaveAgency :: RelativeAgency | |
NobodyHasAgency :: RelativeAgency |
type family Relative (pr :: PeerRole) (a :: Agency) :: RelativeAgency where ... Source #
Compute effective agency with respect to the peer role, for client role, agency is preserved, while for the server role it is flipped.
Equations
data ReflRelativeAgency (a :: Agency) (r :: RelativeAgency) (r' :: RelativeAgency) where Source #
Type equality for RelativeAgency
which also carries information about
agency. It is isomorphic to a product of Agency
singleton and
r :~: r'
, where both r
and r'
have kind RelativeAgency
.
This is a proper type with values used by the Peer
, however they are
hidden by using Network.TypedProtocol.Peer.Client and
Network.TypedProtocol.Peer.Server APIs.
Constructors
ReflClientAgency :: forall (r :: RelativeAgency). ReflRelativeAgency 'ClientAgency r r | |
ReflServerAgency :: forall (r :: RelativeAgency). ReflRelativeAgency 'ServerAgency r r | |
ReflNobodyAgency :: forall (r :: RelativeAgency). ReflRelativeAgency 'NobodyAgency r r |
type WeHaveAgencyProof (pr :: PeerRole) (st :: ps) = ReflRelativeAgency (StateAgency st) 'WeHaveAgency (Relative pr (StateAgency st)) Source #
Type of the proof that we have the agency.
ReflClientAgency
has this type only iff `StateAgency
st ~ ClientAgency'
and `pr ~ AsClient'
.
ReflServerAgency
has this type only iff `StateAgency
st ~ ServerAgency'
and `pr ~ AsServer'
type TheyHaveAgencyProof (pr :: PeerRole) (st :: ps) = ReflRelativeAgency (StateAgency st) 'TheyHaveAgency (Relative pr (StateAgency st)) Source #
Type of the proof that the remote side has the agency.
ReflClientAgency
has this type only iff `StateAgency
st ~ ClientAgency'
and `pr ~ AsServer'
.
ReflServerAgency
has this type only iff `StateAgency
st ~ ServerAgency'
and `pr ~ AsClient'
type NobodyHasAgencyProof (pr :: PeerRole) (st :: ps) = ReflRelativeAgency (StateAgency st) 'NobodyHasAgency (Relative pr (StateAgency st)) Source #
Type of the proof that nobody has agency in this state.
Only ReflNobodyAgency
can fulfil the proof obligation.
FlipAgency
type family FlipAgency (pr :: PeerRole) :: PeerRole where ... Source #
A type function to flip the client and server roles.
Equations
FlipAgency 'AsClient = 'AsServer | |
FlipAgency 'AsServer = 'AsClient |
ActiveState
class IsActiveState (st :: ps) (agency :: Agency) where Source #
A type class which restricts states to ones that have ClientAgency
or
ServerAgency
, excluding NobodyAgency
.
One can use notActive
to eliminate cases for which both
is in scope and for which we have an evidence that the state is not
active (i.e. a singleton). This is useful when writing a IsActiveState
stCodec
.
Methods
activeAgency :: ActiveAgency' st agency Source #
Instances
'ClientAgency ~ StateAgency st => IsActiveState (st :: ps) 'ClientAgency Source # | |
Defined in Network.TypedProtocol.Core Methods activeAgency :: ActiveAgency' st 'ClientAgency Source # | |
'ServerAgency ~ StateAgency st => IsActiveState (st :: ps) 'ServerAgency Source # | |
Defined in Network.TypedProtocol.Core Methods activeAgency :: ActiveAgency' st 'ServerAgency Source # |
type ActiveState (st :: ps) = IsActiveState st (StateAgency st) Source #
A constraint which provides an evidence that the protocol isn't in a terminal state.
notActiveState :: forall ps (st :: ps). (StateAgency st ~ 'NobodyAgency, ActiveState st) => StateToken st -> forall a. a Source #
This is useful function to eliminate cases where the `ActiveState st` is
provided but we are given a state in which neither side has agency
(NobodyAgency
). For example when writing a codec, we only need to encode
/ decode messages which are in active states, but to make such functions
total, notActiveState
needs to be used to eliminate the states in which
nobody has agency.
A good analogy for this function is
.absurd
:: Void
-> a
type ActiveAgency (st :: ps) = ActiveAgency' st (StateAgency st) Source #
Evidence that the protocol isn't in a terminal state.
data ActiveAgency' (st :: ps) (agency :: Agency) where Source #
Evidence that one side of the protocol has the agency, and thus that the protocol hasn't yet terminated.
Constructors
ClientHasAgency :: forall {ps} (st :: ps). StateAgency st ~ 'ClientAgency => ActiveAgency' st 'ClientAgency | Evidence that the client has the agency. |
ServerHasAgency :: forall {ps} (st :: ps). StateAgency st ~ 'ServerAgency => ActiveAgency' st 'ServerAgency | Evidence that the server has the agency. |
Instances
Show (ActiveAgency' st agency) Source # | |
Defined in Network.TypedProtocol.Core Methods showsPrec :: Int -> ActiveAgency' st agency -> ShowS # show :: ActiveAgency' st agency -> String # showList :: [ActiveAgency' st agency] -> ShowS # |
Pipelining
IsPipelined
data IsPipelined where Source #
Promoted data type which indicates if Peer
is used in
pipelined mode or not.
Constructors
Pipelined :: N -> Type -> IsPipelined | Pipelined peer which is using `c :: Type` for collecting responses
from a pipelined messages. |
NonPipelined :: IsPipelined | Non-pipelined peer. |
Outstanding
type family Outstanding (pl :: IsPipelined) :: N where ... Source #
Type level count of the number of outstanding pipelined yields for which
we have not yet collected a receiver result. Used to
ensure that Collect
is only used when there are outstanding results to
collect (e.g. after YieldPipeliend
was used);
and to ensure that the non-pipelined primitives Yield
, Await
and Done
are only used when there are none unsatisfied pipelined requests.
Equations
Outstanding 'NonPipelined = 'Z | |
Outstanding ('Pipelined n _1) = n |
N and Nat
data Nat (n :: N) where Source #
A value level inductive natural number, indexed by the corresponding type
level natural number N
.
This is often needed when writing pipelined peers to be able to count the
number of outstanding pipelined yields, and show to the type checker that
Collect
and Done
are being used correctly.