• No results found

Section 3.6 introduced an interface to a linear EDSL for session-typed channels in which a session of linear type Chan Sis governed by its session typeS:

partition ∶∶ (HasArray sig, Ord α)

⇒ α → (Int,Int) → LStateT sig (Array token α) Int

-- if the range is empty, return the index -- of the largest element < pivot

partition pivot (i,j) | i ≥ j = do a ← readT i

if a < pivot then return i else return (i-1)

-- otherwise, if arr[i]<pivot, recurse on the range (i+1,j) -- and if arr[i]>pivot, move array[i] to the right-hand-side -- of the array and recurse on the range (i,j-1)

| otherwise = do a ← readT i

if a < pivot then partition pivot (i+1,j) else do swap i j

partition pivot (i,j-1) quicksort ∶∶ (HasArray sig, Ord α)

⇒ LStateT sig (Array token α) () quicksort = do len ← sizeT

if len ≤ 1 then return () else do pivot ← readT 0

idx ← partition pivot (1,len-1) swap 0 idx

slice3 idx quicksort

Figure 4.6: Linear quicksort algorithm

In that setting, a channel can either send a value (<!>), receive a value (<?>), offer a choice between two protocols (<&>), or make a choice between two protocols (<+>). There, a channel is a linear data type, and the domain-specific operations send,receive,etc. are used to maintain these channels.

Caires and Pfenning (2010) present an alternative interpretation of session types through the lens of the Curry-Howard correspondence. They claim that any linear type can be viewed as a session protocol, according to the following correspondence:

linear type session type protocol

σ⊸τ σ⟨?⟩τ input σ and continue as τ σ⊗τ σ⟨!⟩τ output σ and continue as τ

σ&τ σ⟨&⟩τ offer choice betweenσ andτ σ⊕τ σ⟨+⟩τ make choice betweenσ andτ

A linear expression ∆⊢e∶τ can be interpreted as a process communicating over channels x∶σ∈∆, as well as over an output channel of type τ.

In this section we implement Caires and Pfenning’s type system, not by adding new operations to send and receive over channels, but instead by implementing the usual linear logical connectives as processes that communicate over linear variables.

4.6.1 Example: an echo server

As in Section 3.6, we can implement an echo server, which takes a string as input and echoes it back to the user.

type EchoProtocol = Lower String⊸Lower String ⊗ LUnit

An echo server is a suspended computation of type EchoProtocol.

server ∶∶ HasMALL⇒Lift exp EchoProtocol server = suspend . λbangˆ $ \s→ put s ⊗ unit

A client is one who uses the EchoProtocol—she sends the string “Testing” to the server, and checks whether she receives the same string back.

client ∶∶ HasMALL⇒Lift exp (EchoProtocol⊸Lower Bool) client = suspend . λˆ $ \s→ s ∧

put "Testing" `letpair` \(x,y)→ y `letUnit` x >! \s→

put $ s == "Testing"

4.6.2 Implementation

Although we use the same syntax as the pure linear lambda calculus, what we really want is an implementation that communicates data over channels. Since the type of a ses- sion changes over time, sessions should be implemented using untyped channels. We use

unsafeCoerceto send and receive typed data over these untyped channels—the linear pro- tocols will ensure that whenever a value of typeσ is sent on the channel, it will be coerced back into the same typeσ by the recipient.

A session is implemented as a pair UChan of untyped channels. We use a pair so that each component has a fixed direction—the left channel will always be used to receive data, and right channel will always be used to send data. Every time we construct a UChan, we also construct its swap, which corresponds to the other end of the channel.

type UChan = (Chan Any, Chan Any) newU ∶∶ IO (UChan,UChan)

newU = do c1 ← IO.newChan c2 ← IO.newChan

return ((c1,c2),(c2,c1))

These channels are untyped, and we useunsafeCoerceto send and receive data of arbitrary types. As long asrecvU is only instantiated at types inferred from the session protocol, it should never cause a segfault.

sendU ∶∶ UChan →α→ IO ()

sendU (cin,cout) a = writeChan cout $ unsafeCoerce a recvU ∶∶ UChan → IO α

recvU (cin,cout) = unsafeCoerce <$> readChan cin

The operation linkU takes as input two channels and forwards data between them in both directions.

linkU ∶∶ UChan → UChan → IO ()

linkU c1 c2 = (forkIO $ forward c1 c2) ≫ forward c2 c1 where

forward c c' = recvU c >>= sendU c' ≫ forward c c'

A new shallow embedding. We use a variant of the shallow embedding to encode expressions. An expression is a function from evaluation contexts plus an extra UChan to IO (). The extra UChan is the output channel of the expressions; an expression of type σ⊗τ will send a value of type σ on its output channel.

data Sessions γ τ = SExp {runSExp ∶∶ ECtx Sessions γ→ UChan → IO ()} data instance Effect Sessions = IO

Values in this setting, no matter the type, are allUChan’s.

data instance LVal Sessions τ = Chan UChan

To evaluate an expression, we first construct a newUChan, which gives us two endpoints to a new channel. We pass one of the endpoints to the expression using runSExp, and the

other endpoint is returned as the value of the expression.

instance Eval Sessions where eval e ρ = do (c,c') ← newU

forkIO $ runSExp e ρ c return $ Chan c'

The HasLower type class. To construct an expression of type Lower τ via put a, we simply send the Haskell valueaover the output channel.

put a = SExp $ \_ c → sendU c a

To implemente >! f, we spawn a new channel and pass one end to e. We wait for a value from the other end, to which we applyf.

e >! f = SExp $ \ρ c → do let (ρ1,ρ2) = split ρ (x,x')← newU

forkIO $ runSExp e ρ1 x a ← recvU x'

runSExp (f a) ρ2 c

Sending and receiving values. An expression of type σ⊸τ is a process that receives a channel of type σ and then continues asτ. So the expression ˆλx.e receives a value on its output channel and binds that value tox in the continuation.

ˆ

λ f = SExp $ \(ρ ∶∶ ECtx Sessions γ) c → do v← recvU c

let x = (Proxy ∶∶ Proxy (Fresh γ)) runSExp (f $ var x) (add x v ρ) c An application e1ˆe2 needs to connect the output of e2 to the input of e1. The process

starts by creating a new channel, x, on which e2 can send its output. Next, we send the

other end of the channel, calledx′, to e1, which we do by initializing another new channel

y. One endpoint of y is passed to e1, and then the remainder is forwarded along to the

original output of the process.

e1 ∧ e2 = SExp $ \ρ c → do let (ρ1,ρ2) = split ρ

(x,x') ← newU

forkIO $ runSExp e2 ρ2 x -- e2 sends output to x

(y,y') ← newU

sendU y' (Chan x') -- send x' to e1 via y forkIO $ runSExp e1 ρ1 y -- e1 receives input from y

linkU c y' -- e1 forwards result to (e1 ∧ e2)

The implementation of the HasTensor type class is exactly dual to HasLolli—where ⊸receives a value, ⊗ sends a value, and vice versa.

Making and offering choices. A process of typeσ1&σ2accepts a boolean flag as input,

eitherLeft () orRight (), to indicate which ofσ1 orσ2 to continue as.

instance HasWith Sessions where

e1 & e2 = SExp $ \ρ (c ∶∶ UChan) → recvU c >>= \case Left () → runSExp e1 ρ c

Right () → runSExp e2 ρ c

proj1 e = SExp $ \ρ c → do sendU c (Left ()) runSExp e ρ c proj2 e = SExp $ \ρ c → do sendU c (Right ())

runSExp e ρ c The HasPlusclass is exactly the dual.