224Asynchronous Effects
DANEL AHMAN and MATIJA PRETNAR,
University of Ljubljana, SloveniaWe explore asynchronous programming with algebraic effects. We complement their conventional synchronoustreatment by showing how to naturally also accommodate asynchrony within them, namely, by decouplingthe execution of operation calls into signalling that an operation’s implementation needs to be executed,and interrupting a running computation with the operation’s result, to which the computation can react byinstalling interrupt handlers. We formalise these ideas in a small core calculus, called 𝜆 æ . We demonstrate theflexibility of 𝜆 æ using examples ranging from a multi-party web application, to preemptive multi-threading, toremote function calls, to a parallel variant of runners of algebraic effects. In addition, the paper is accompaniedby a formalisation of 𝜆 æ ’s type safety proofs in Agda, and a prototype implementation of 𝜆 æ in OCaml.CCS Concepts: • Theory of computation → Concurrency ; Program constructs ; Program semantics .Additional Key Words and Phrases: algebraic effects, asynchrony, concurrency, interrupt handling, signals.
ACM Reference Format:
Danel Ahman and Matija Pretnar. 2021. Asynchronous Effects.
Proc. ACM Program. Lang.
5, POPL, Article 24(January 2021), 28 pages. https://doi.org/10.1145/3434305
Effectful programming abstractions are at the heart of many modern general-purpose programminglanguages. They can increase expressiveness by giving access to first-class continuations, but oftensimply help users to write cleaner code, e.g., by avoiding having to manage a program’s memoryexplicitly in state-passing style, or getting lost in callback hell while programming asynchronously.An increasing number of language designers and programmers are starting to embrace algebraiceffects , where one uses algebraic operations [Plotkin and Power 2002] and effect handlers [Plotkinand Pretnar 2013] to uniformly and user-definably express a wide range of effectful behaviour,ranging from basic examples such as state, rollbacks, exceptions, and nondeterminism [Bauerand Pretnar 2015], to advanced applications in concurrency [Dolan et al. 2018] and statisticalprobabilistic programming [Bingham et al. 2019], and even quantum computation [Staton 2015].While covering many examples, the conventional treatment of algebraic effects is synchronous by nature. In it effects are invoked by placing operation calls in one’s code, which then propagateoutwards until they trigger the actual effect, finally yielding a result to the rest of the computationthat has been waiting the whole time. While blocking the computation is indeed sometimes needed,e.g., in the presence of general effect handlers that can execute their continuation any number oftimes, it forces all uses of algebraic effects to be synchronous, even when this is not necessary, e.g.,when the effect involves executing a remote query to which a response is not needed (immediately).Motivated by the recent interest in the combination of asynchrony and algebraic effects [Dolanet al. 2018; Leijen 2017], we explore what it takes (in terms of language design, safe programmingabstractions, and a self-contained core calculus) to accompany the synchronous treatment of
Authors’ address: Danel Ahman, [email protected]; Matija Pretnar, [email protected], University ofLjubljana, Faculty of Mathematics and Physics, Jadranska 21, Ljubljana, SI-1000, Slovenia.This work is licensed under a Creative Commons Attribution 4.0 International License© 2021 Copyright held by the owner/author(s).2475-1421/2021/1-ART24https://doi.org/10.1145/3434305 Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. a r X i v : . [ c s . P L ] N ov algebraic effects with an asynchronous one. At the heart of our approach is the decoupling of theexecution of operation calls into signalling that some implementation of an operation needs to beexecuted, and interrupting a running computation with its result, to which the computation canreact by installing interrupt handlers . Importantly, we show that our approach is flexible enoughthat not all signals need to have a corresponding interrupt, and vice versa, allowing us to also model spontaneous behaviour , such as a user clicking a button or the environment preempting a thread.While we are not the first ones to work on asynchrony for algebraic effects, the prior work in thisarea (in the context of general effect handlers) has achieved it by delegating the actual asynchronyto the respective language backends [Dolan et al. 2018; Leijen 2017]. In contrast, in this paper wedemonstrate how to capture the combination of asynchrony and algebraic effects in a self-contained core calculus. It is important to emphasise that our aim is not to replace general effect handlers, butinstead to complement them with robust primitives tailored to asynchrony—our proposed approachis algebraic by design, so as to be ready for future extensions with general effect handlers. Paper structure.
In Section 2, we give a high-level overview of our approach to asynchrony foralgebraic effects. In Section 3 and 4, we distil our ideas into a core calculus, 𝜆 æ , equipped with asmall-step semantics, a type-and-effect system, and proofs of type safety. In Section 5, we show 𝜆 æ in action on examples such as preemptive multi-threading, remote function calls, and a parallelvariant of runners of algebraic effects. We conclude, and discuss related and future work in Section 6. Code.
The paper is accompanied by a formalisation of 𝜆 æ ’s type safety proofs in Agda [Ahman2020], and a prototype implementation of 𝜆 æ in OCaml, called Æff [Pretnar 2020]. For ease of use,we provide them both also as a single virtual machine image [Ahman and Pretnar 2020].In the Agda formalisation, we consider only well-typed syntax of a variant of 𝜆 æ in which thesubsumption rule manifests as an explicit coercion, so as to make working with de Bruijn indicesless painful. Meanwhile, the Æff implementation provides an interpreter and a simple typechecker,but it does not yet support inferring and checking effect annotations. In addition, Æff providesa web interface that allows users to enter their programs and interactively click through theirexecutions. Æff also comes with implementations of all the examples we present in this paper.Separately, Poulson [2020] has shown how to implement 𝜆 æ in Frank [Convent et al. 2020]. We begin with a high-level overview of how we accommodate asynchrony within algebraic effects.
We first recall the basic ideas of programming with algebraic effects, illustrating that their traditionaltreatment is synchronous by nature. For an in-depth overview, we refer to the tutorial by Pretnar[2015], and to the seminal papers of the field [Plotkin and Power 2002; Plotkin and Pretnar 2013].In this algebraic treatment, sources of computational effects are modelled using signatures of operation symbols op : 𝐴 op → 𝐵 op . For instance, one models 𝑆 -valued state using two operations, get : → 𝑆 and set : 𝑆 → ; and 𝐸 -valued exceptions using a single operation raise : 𝐸 → .Programmers can then invoke the effect that an op : 𝐴 op → 𝐵 op models by placing an operationcall op ( 𝑉 , 𝑦.𝑀 ) in their code. Here, the parameter value 𝑉 has type 𝐴 op , and the variable 𝑦 , whichis bound in the continuation 𝑀 , has type 𝐵 op . For instance, for set , the parameter 𝑉 would be thenew value of the store, and for get , the variable 𝑦 would be bound to the current value of the store.A program written in terms of operation calls is by itself just an inert piece of code. To executeit, programmers have to provide implementations for the operation calls appearing in it. The ideais that an implementation of op ( 𝑉 , 𝑦.𝑀 ) takes 𝑉 as its input, and its output gets bound to 𝑦 . Forinstance, this could take the form of defining a suitable effect handler [Plotkin and Pretnar 2013], Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:3 but could also be given by calls to runners of algebraic effects [Ahman and Bauer 2020], or simplyby invoking some (default) top-level (native) implementation. What is important is that somepre-defined piece of code 𝑀 op [ 𝑉 / 𝑥 ] gets executed in place of every operation call op ( 𝑉 , 𝑦.𝑀 ) .Now, what makes the conventional treatment of algebraic effects synchronous is that the executionof an operation call op ( 𝑉 , 𝑦.𝑀 ) blocks until some implementation of op returns a value 𝑊 to bebound to 𝑦 , so that the execution of the continuation 𝑀 [ 𝑊 / 𝑦 ] could proceed [Bauer and Pretnar2014; Kammar et al. 2013]. Conceptually, this kind of blocking behaviour can be illustrated as 𝑀 op [ 𝑉 / 𝑥 ] (cid:123) ∗ return 𝑊 (cid:15) (cid:15) . . . (cid:123) op ( 𝑉 , 𝑦.𝑀 ) (cid:79) (cid:79) 𝑀 [ 𝑊 / 𝑦 ] (cid:123) . . . (1)where return 𝑊 is a computation that causes no effects and simply returns the given value 𝑊 .While blocking the rest of the computation is needed in the presence of general effect handlersthat can execute their continuation any number of times, it forces all uses of algebraic effects to besynchronous, even when this is not necessary, e.g., when the effect in question involves executinga remote query to which a response is not needed immediately, or sometimes never at all.In the rest of this section, we describe how we decouple the invocation of an operation call fromthe act of receiving its result, and how we give programmers a means to block execution only whenit is necessary. While we end up surrendering some of effect handlers’ generality, such as havingaccess to the continuation that captures the rest of the computation to be handled, then in returnwe get a natural and robust formalism for asynchronous programming with algebraic effects. We begin by observing that the execution of an operation call op ( 𝑉 , 𝑦.𝑀 ) , as shown in (1), consistsof three distinct phases : (i) signalling that an implementation of op needs to be executed withparameter 𝑉 (the up-arrow), (ii) executing this implementation (the horizontal arrow), and (iii)interrupting the blocking of 𝑀 with a value 𝑊 (the down-arrow). In order to overcome the unwantedside-effects of blocking execution on every operation call, we shall naturally decouple these phasesinto separate programming concepts, allowing the execution of 𝑀 to proceed even if (ii) has notyet completed and (iii) taken place. In particular, we decouple an operation call into issuing an outgoing signal , written ↑ op ( 𝑉 , 𝑀 ) , and receiving an incoming interrupt , written ↓ op ( 𝑊 , 𝑀 ) .It is important to note that while we have used the execution of operation calls to motivate theintroduction of signals and interrupts as programming concepts, not all issued signals need to have acorresponding interrupt response , and not all interrupts need to be responses to issued signals , allowingus to also model spontaneous behaviour, such as the environment preempting a thread.When issuing a signal ↑ op ( 𝑉 , 𝑀 ) , the value 𝑉 is a payload , such as a location to be looked up or amessage to be displayed, aimed at whoever is listening for the given signal. We use the ↑ -notation toindicate that signals issued in sub-computations propagate outwards—in this sense signals behavejust like conventional operation calls. However, signals crucially differ from conventional operationcalls in that no additional variables are bound in the continuation 𝑀 , making it naturally possibleto continue executing 𝑀 straight after the signal has been issued, e.g., as depicted below: . . . (cid:123) ↑ op ( 𝑉 , 𝑀 ) op 𝑉 (cid:79) (cid:79) (cid:123) 𝑀 (cid:123) . . . As a running example , consider a computation 𝑀 feedClient , which lets a user scroll through aseemingly infinite feed, e.g., by repeatedly clicking a “next page” button. For efficiency, 𝑀 feedClient does not initially cache all the data, but instead requests a new batch of data each time the user is Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. nearing the end of the cache. To communicate with the outside world, 𝑀 feedClient can issue a signal ↑ request ( cachedSize + , 𝑀 feedClient ) to request a new batch of data starting from the end of the current cache, or a different signal ↑ display ( message , 𝑀 feedClient ) to display a message to the user. In both cases, the continuation does not wait for an acknowledge-ment that the signal was received, but instead continues to provide a seamless experience to theuser. It is however worth noting that these signals differ in what 𝑀 feedClient expects of them: to the request signal, it expects a response at some future point in its execution, while it does not expectany response to the display signal, illustrating that not every issued signal needs a response.When the outside world wants to get the attention of a computation, be it in response to a signalor spontaneously, it happens by propagating an interrupt ↓ op ( 𝑊 , 𝑀 ) to the computation. Here,the value 𝑊 is again a payload, while 𝑀 is the computation receiving the interrupt. It is importantto note that unlike signals, interrupts are not triggered by the computation itself, but are insteadissued by the outside world , and can thus interrupt any sequence of evaluation steps, e.g., as in op 𝑊 (cid:15) (cid:15) . . . (cid:123) 𝑀 (cid:123) ↓ op ( 𝑊 , 𝑀 ) (cid:123) . . . In our running example, there are two interrupts of interest: ↓ response ( newBatch , 𝑀 ) , whichdelivers new data to replenish the cache; and ↓ nextItem (() , 𝑀 ) , with which the user requests to seethe next data item. In both cases, 𝑀 represents the state of 𝑀 feedClient before the interrupt arrived.We use the ↓ -notation to indicate that interrupts propagate inwards into sub-computations, tryingto reach anyone listening for them, and only get discarded when they reach a return . It is worthnoting that programmers are not expected to write interrupts explicitly in their programs—instead,interrupts are usually induced by signals issued by other parallel processes, as explained next. As noted above, the computations we consider do not evolve in isolation, instead they also commu-nicate with the outside world, by issuing outgoing signals and receiving incoming interrupts.We model the outside world by composing individual computations into parallel processes
𝑃, 𝑄, . . . .To keep the presentation clean and focussed on the asynchronous use of algebraic effects, weconsider a very simple model of parallelism: a process is either one of the individual computationsbeing run in parallel, written run 𝑀 , or the parallel composition of two processes, written 𝑃 || 𝑄 .To capture the signals and interrupts based interaction of processes, our operational semanticsincludes rules for propagating outgoing signals from individual computations to processes, turningprocesses’ outgoing signals into incoming interrupts for their surrounding world, and propagatingincoming interrupts from processes to individual computations. For instance, in our running example, 𝑀 feedClient ’s request for new data is executed as follows (with the active redexes highlighted): run (↑ request ( 𝑉 , 𝑀 feedClient )) || run 𝑀 feedServer (cid:123) (↑ request ( 𝑉 , run 𝑀 feedClient )) || run 𝑀 feedServer (cid:123) ↑ request (cid:0) 𝑉 , run 𝑀 feedClient || ↓ request ( 𝑉 , run 𝑀 feedServer ) (cid:1) (cid:123) ↑ request (cid:0) 𝑉 , run 𝑀 feedClient || run (↓ request ( 𝑉 , 𝑀 feedServer )) (cid:1) Here, the first and the last reduction step respectively propagate signals outwards and interruptsinwards. The middle reduction step corresponds to what we call a broadcast rule —it turns anoutward moving signal in one of the processes into an inward moving interrupt for the processparallel to it, while continuing to propagate the signal outwards to any further parallel processes.
Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:5
So far, we have shown that our computations can issue outgoing signals and receive incominginterrupts, and how these evolve when executing parallel processes, but we have not yet saidanything about how computations can actually react to incoming interrupts of interest.In order to react to incoming interrupts, our computations can install interrupt handlers , written promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 that should be read as: “we promise to handle a future interrupt named op using the computation 𝑀 in the continuation 𝑁 , with 𝑥 bound to the payload of the interrupt”. Fulfilling this promise consistsof executing 𝑀 and binding its result to the variable 𝑝 in 𝑁 . This is captured by the reduction rule ↓ op ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) (cid:123) let 𝑝 = 𝑀 [ 𝑉 / 𝑥 ] in ↓ op ( 𝑉 , 𝑁 ) It is worth noting two things: the interrupt handler is not reinstalled by default , and the interruptitself keeps propagating inwards into the sub-computation 𝑁 . Regarding the former, programmerscan selectively reinstall interrupt handlers when needed, by defining them suitably recursively,e.g., as we demonstrate in Section 2.6. Concerning the latter, then in order to skip certain interrupthandlers for some op , one can carry additional data in op ’s payload (e.g., a thread ID) and thencondition the (non-)triggering of those interrupt handlers on this data, e.g., as we do in Section 5.1.Interrupts that do not match a given interrupt handler ( op ≠ op ′ ) are simply propagated past it: ↓ op ′ ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) (cid:123) promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in ↓ op ′ ( 𝑉 , 𝑁 ) Interrupt handlers differ from operation calls in two important aspects. First, they enable user-sidepost-processing of received data, using 𝑀 , while in operation calls the result is immediately boundin the continuation. Second, and more importantly, their semantics is non-blocking . In particular, 𝑁 (cid:123) 𝑁 ′ implies promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 (cid:123) promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ′ meaning that the continuation 𝑁 , and thus the whole computation, can make progress even thoughno incoming interrupt op has been propagated to the computation from the outside world.As the observant reader might have noticed, the non-blocking behaviour of interrupt handlingmeans that our operational semantics has to work on open terms because the variable 𝑝 can appearfree in both 𝑁 and 𝑁 ′ above. However, it is important to note that 𝑝 is not an arbitrary variable,but in fact gets assigned a distinguished promise type ⟨ 𝑋 ⟩ for some value type 𝑋 —we shall cruciallymake use of this typing of 𝑝 in the proof of type safety for our 𝜆 æ -calculus (see Theorem 3.3). As noted earlier, installing an interrupt handler means making a promise to handle a given interruptin the future. To check that an interrupt has been received and handled, we provide program-mers a means to selectively block execution and await a specific promise to be fulfilled, written await 𝑉 until ⟨ 𝑥 ⟩ in 𝑀 , where if 𝑉 has a promise type ⟨ 𝑋 ⟩ , the variable 𝑥 bound in 𝑀 has type 𝑋 .Importantly, the continuation 𝑀 is executed only when the await is handed a fulfilled promise ⟨ 𝑉 ⟩ : await ⟨ 𝑉 ⟩ until ⟨ 𝑥 ⟩ in 𝑀 (cid:123) 𝑀 [ 𝑉 / 𝑥 ] Revisiting our example of scrolling through a seemingly infinite feed, 𝑀 feedClient could use await to block until it has received an initial configuration, such as the batch size used by 𝑀 feedServer .As the terminology suggests, this part of 𝜆 æ is strongly influenced by existing work on futuresand promises [Schwinghammer 2002] for structuring concurrent programs, and their use in modernlanguages, such as in Scala [Haller et al. 2020]. While prior work often models promises as writable,single-assignment references, we instead use the substitution of values for ordinary immutablevariables (of distinguished promise type) to model that a promise gets fulfilled exactly once. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
Finally, we show how to implement our example of scrolling through a seemingly infinite feed.For a simpler exposition, we allow ourselves access to mutable references, though the same can beachieved by rolling one’s own state. Further, we use ↑ op 𝑉 as a syntactic sugar for ↑ op ( 𝑉 , return ()) . We implement the client computation 𝑀 feedClient as the function client defined below.For presentation purposes, we split the definition of client between multiple code blocks.First, the client sets up the initial values of the auxiliary references, issues a signal to the serverasking for the data batch size that it uses, and then installs a corresponding interrupt handler: let client () =let (cachedData , requestInProgress , currentItem) = (ref [] , ref false , ref 0) in ↑ batchSizeRequest () ; promise (batchSizeResponse batchSize ↦→ return ⟨ batchSize ⟩ ) as batchSizePromise in While the server is asynchronously responding to the batch size request, the client sets up anauxiliary function requestNewData , which it later uses to request new data from the server: let requestNewData offset =requestInProgress := true ; ↑ request offset ; promise (response newBatch ↦→ cachedData := !cachedData @ newBatch ; requestInProgress := false ; return ⟨ () ⟩ ) as _ in return ()in Here, the client first sets a flag indicating that a new data request is in process, then issues a request signal to the server, and finally installs an interrupt handler that updates the cache oncethe response interrupt arrives. Note that the client does not block while awaiting new data, insteadit continues executing, notifying the user to wait and try again once the cache is empty (see below).Then, the client sets up its main loop, which is a simple recursively defined interrupt handler: let rec clientLoop batchSize =promise (nextItem () ↦→ let cachedSize = length !cachedData in(if (!currentItem > cachedSize − batchSize / 2) && (not !requestInProgress) thenrequestNewData (cachedSize + 1)elsereturn ()) ; (if !currentItem < cachedSize then ↑ display (toString (nth !cachedData !currentItem)) ; currentItem := !currentItem + 1else ↑ display "please wait a bit and try again") ; clientLoop batchSize) as p in return pin In it, the client listens for a nextItem interrupt from the user to display more data. Once the interruptarrives, the client checks if its cache is becoming empty—if so, it uses the requestNewData functionto request more data from the server. Next, if there is still some data in the cache, the client issues
Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:7 a signal to display the next data item to the user. If however the cache is empty, the client issues asignal to display a waiting message to the user. The client then simply recursively reinvokes itself.As a last step of setting itself up, the client blocks until the server has responded with the batchsize it uses, after which the client starts its main loop with the received batch size as follows: await batchSizePromise until ⟨ batchSize ⟩ in clientLoop batchSize We implement the server computation 𝑀 feedServer as the following function: let server batchSize =let rec waitForBatchSize () =promise (batchSizeRequest () ↦→↑ batchSizeResponse batchSize ; waitForBatchSize ()) as p in return pinlet rec waitForRequest () =promise (request offset ↦→ let payload = map (fun x ↦→
10 ∗ x) (range offset (offset + batchSize − 1)) in ↑ response payload ; waitForRequest ()) as p in return pinwaitForBatchSize () ; waitForRequest () where the computation range i j returns a list of integers ranging from i to j (both inclusive).The server simply installs two recursively defined interrupt handlers: the first one listens for andresponds to client’s requests about the batch size it uses; and the second one responds to client’srequests for new data. Both interrupt handlers then simply recursively reinstall themselves. We can also simulate the user as a computation. Namely, we implement it as a functionthat every now and then issues a request to the client to display the next data item: let rec user () =let rec wait n =if n = 0 then return () else wait (n − 1)in ↑ nextItem () ; wait 10 ; user () It is straightforward to extend the user also with a handler for display interrupts (we omit it here).
Finally, we can simulate our running examplein full by running all three computations we defined above as parallel processes, e.g., as follows: run (server 42) || run (client ()) || run (user ()) We now distil the ideas we introduced in the previous section into a core calculus for programmingwith asynchronous effects, called 𝜆 æ . It is based on Levy et al.’s [2003] fine-grain call-by-value 𝜆 -calculus (FGCBV), and as such, it is a low-level intermediate language to which a correspondinghigh-level user-facing programming language could be compiled to. In order to better explain thedifferent features of the calculus and its semantics, we split 𝜆 æ into a sequential part (discussed below)and a parallel part (discussed in Section 4). We note that this separation is purely presentational. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
Values
𝑉 ,𝑊 :: = 𝑥 variable (cid:12)(cid:12) () (cid:12)(cid:12) ( 𝑉 ,𝑊 ) unit and pair (cid:12)(cid:12) inl 𝑌 𝑉 (cid:12)(cid:12) inr 𝑋 𝑉 left and right injections (cid:12)(cid:12) fun ( 𝑥 : 𝑋 ) ↦→ 𝑀 function abstraction (cid:12)(cid:12) ⟨ 𝑉 ⟩ fulfilled promise Computations
𝑀, 𝑁 :: = return 𝑉 returning a value (cid:12)(cid:12) let 𝑥 = 𝑀 in 𝑁 sequencing (cid:12)(cid:12) let rec 𝑓 𝑥 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) = 𝑀 in 𝑁 recursive definition (cid:12)(cid:12) 𝑉 𝑊 function application (cid:12)(cid:12) match 𝑉 with {( 𝑥, 𝑦 ) ↦→ 𝑀 } product elimination (cid:12)(cid:12) match 𝑉 with {} 𝑍 ! ( 𝑜,𝜄 ) empty elimination (cid:12)(cid:12) match 𝑉 with { inl 𝑥 ↦→ 𝑀, inr 𝑦 ↦→ 𝑁 } sum elimination (cid:12)(cid:12) ↑ op ( 𝑉 , 𝑀 ) outgoing signal (cid:12)(cid:12) ↓ op ( 𝑉 , 𝑀 ) incoming interrupt (cid:12)(cid:12) promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 interrupt handler (cid:12)(cid:12) await 𝑉 until ⟨ 𝑥 ⟩ in 𝑀 awaiting a promise to be fulfilled Fig. 1. Values and computations.
The syntax of terms is given in Figure 1, stratified into values and computations , as in FGCBV.
Values.
The values
𝑉 ,𝑊 , . . . are mostly standard. They include variables, introduction formsfor sums and products, and functions. The only 𝜆 æ -specific value is ⟨ 𝑉 ⟩ , which denotes a fulfilledpromise , indicating that the promise of handling some interrupt has been fulfilled with the value 𝑉 . Computations.
The computations
𝑀, 𝑁 , . . . also include all standard terms from FGCBV: returningvalues, sequencing, recursion, function application, and elimination forms. Note that we annotaterecursive definitions with the type of the function being defined, including the annotations ( 𝑜, 𝜄 ) that describe the possible effects of the function. While we do not study effect inference in thispaper, our experience is that these annotations should make it possible to fully infer types.The first two computations specific to 𝜆 æ are signals ↑ op ( 𝑉 , 𝑀 ) and interrupts ↓ op ( 𝑉 , 𝑀 ) , wherethe name op is drawn from an assumed set Σ of names, 𝑉 is a data payload, and 𝑀 is a continuation.The next 𝜆 æ -specific computation is the interrupt handler promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ,where 𝑥 is bound in 𝑀 and 𝑝 in 𝑁 . As discussed in the previous section, one should understandthis computation as making a promise to handle a future incoming interrupt op by executingthe computation 𝑀 . Sub-computations of the continuation 𝑁 can then explicitly await, whennecessary, this promise to be fulfilled by blocking on the promise variable 𝑝 using the final 𝜆 æ -specific computation term, the awaiting construct await 𝑉 until ⟨ 𝑥 ⟩ in 𝑀 . We note that 𝑝 is anordinary variable—it just gets assigned the distinguished promise type by the interrupt handler. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:9
Standard computation rules ( fun ( 𝑥 : 𝑋 ) ↦→ 𝑀 ) 𝑉 (cid:123) 𝑀 [ 𝑉 / 𝑥 ] let 𝑥 = ( return 𝑉 ) in 𝑁 (cid:123) 𝑁 [ 𝑉 / 𝑥 ] match ( 𝑉 ,𝑊 ) with {( 𝑥, 𝑦 ) ↦→ 𝑀 } (cid:123) 𝑀 [ 𝑉 / 𝑥,𝑊 / 𝑦 ] match ( inl 𝑌 𝑉 ) with { inl 𝑥 ↦→ 𝑀, inr 𝑦 ↦→ 𝑁 } (cid:123) 𝑀 [ 𝑉 / 𝑥 ] match ( inr 𝑋 𝑊 ) with { inl 𝑥 ↦→ 𝑀, inr 𝑦 ↦→ 𝑁 } (cid:123) 𝑁 [ 𝑊 / 𝑦 ] let rec 𝑓 𝑥 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) = 𝑀 in 𝑁 (cid:123) 𝑁 [ fun ( 𝑥 : 𝑋 ) ↦→ let rec 𝑓 𝑥 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) = 𝑀 in 𝑀 / 𝑓 ] Algebraicity of signals and interrupt handlers let 𝑥 = (↑ op ( 𝑉 , 𝑀 )) in 𝑁 (cid:123) ↑ op ( 𝑉 , let 𝑥 = 𝑀 in 𝑁 ) let 𝑥 = ( promise ( op 𝑦 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) in 𝑁 (cid:123) promise ( op 𝑦 ↦→ 𝑀 ) as 𝑝 in ( let 𝑥 = 𝑁 in 𝑁 ) Commutativity of signals with interrupt handlers promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in ↑ op ′ ( 𝑉 , 𝑁 ) (cid:123) ↑ op ′ ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) Interrupt propagation ↓ op ( 𝑉 , return 𝑊 ) (cid:123) return 𝑊 ↓ op ( 𝑉 , ↑ op ′ ( 𝑊 , 𝑀 )) (cid:123) ↑ op ′ ( 𝑊 , ↓ op ( 𝑉 , 𝑀 ))↓ op ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) (cid:123) let 𝑝 = 𝑀 [ 𝑉 / 𝑥 ] in ↓ op ( 𝑉 , 𝑁 )↓ op ′ ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) (cid:123) promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in ↓ op ′ ( 𝑉 , 𝑁 ) ( op ≠ op ′ ) Awaiting a promise to be fulfilled await ⟨ 𝑉 ⟩ until ⟨ 𝑥 ⟩ in 𝑀 (cid:123) 𝑀 [ 𝑉 / 𝑥 ] Evaluation context rule 𝑀 (cid:123) 𝑁 E [ 𝑀 ] (cid:123) E [ 𝑁 ] where E :: = [ ] (cid:12)(cid:12) let 𝑥 = E in 𝑁 (cid:12)(cid:12) ↑ op ( 𝑉 , E) (cid:12)(cid:12) ↓ op ( 𝑉 , E) (cid:12)(cid:12) promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in E Fig. 2. Small-step operational semantics of computations.
We equip 𝜆 æ with an evaluation contexts based small-step operational semantics, defined using areduction relation 𝑀 (cid:123) 𝑁 . The reduction rules and evaluation contexts are given in Figure 2. Computation rules.
The first group includes standard reduction rules from FGCBV, such as 𝛽 -reducing function applications, sequential composition, and the standard elimination forms. Thesemantics also includes a rule for unfolding general-recursive definitions. These rules involvestandard capture avoiding substitutions 𝑀 [ 𝑉 / 𝑥 ] , defined by straightforward structural recursion. Algebraicity.
This group of reduction rules propagates outwards the signals (resp. interrupthandlers) that have been issued (resp. installed) in sub-computations. While it is not surprisingthat outgoing signals behave like algebraic operation calls , getting propagated outwards as far aspossible, then it is much more curious that the natural operational behaviour of interrupt handlersturns out to be the same. As we shall explain in Section 6, despite using the (systems-inspired)“handler” terminology, mathematically interrupt handlers are in fact a form of algebraic operations.
Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
Commutativity of signals with interrupt handlers.
This rule complements the algebraicity rule forsignals, by further propagating them outwards, past any enveloping interrupt handlers. From theperspective of algebraic effects, this rule is an example of two algebraic operations commuting . Forthis rule to be type safe, the type system ensures that the (promise) variable 𝑝 cannot appear in 𝑉 . Interrupt propagation.
The handler-operation curiosity does not end with interrupt handlers. Thisgroup of reduction rules describes how interrupts are propagated inwards into sub-computations.While ↓ op ( 𝑉 , 𝑀 ) might look like a conventional operation call, then its operational behaviourinstead mirrors that of (deep) effect handling , where one also recursively descends into the compu-tation being handled. The first reduction rule states that we can safely discard an interrupt when itreaches a terminal computation return 𝑊 . The second rule states that we can propagate incominginterrupts past any outward moving signals. The last two rules describe how interrupts interactwith interrupt handlers, in particular, that the former behave like effect handling (when under-standing interrupt handlers as generalised algebraic operations). On the one hand, if the interruptmatches the interrupt handler it encounters, the corresponding handler code 𝑀 is executed, andthe interrupt is propagated inwards into the continuation 𝑁 . On the other hand, if the interruptdoes not match the interrupt handler, it is simply propagated past the interrupt handler into 𝑁 . Awaiting.
The semantics includes a 𝛽 -rule for the await construct, allowing the blocked compu-tation 𝑀 to proceed executing as 𝑀 [ 𝑉 / 𝑥 ] when await is given a fulfilled promise ⟨ 𝑉 ⟩ . Evaluation contexts.
The semantics allows reductions under evaluation contexts E . Observe thatas discussed earlier, the inclusion of interrupt handlers in the evaluation contexts means thatreductions involve potentially open terms. Also, differently from the semantics of conventionaloperation calls [Bauer and Pretnar 2014; Kammar et al. 2013], our evaluation contexts includeoutgoing signals. As such, the evaluation context rule allows the execution of a computation toproceed even if a signal has not yet been propagated to its receiver, or when an interrupt has notyet arrived. Importantly, the evaluation contexts do not include await , so as to model its intendedblocking behaviour. We write E [ 𝑀 ] for the recursive operation of filling the hole [ ] in E with 𝑀 . Non-confluence.
It is worth noting that the asynchronous design means that the operationalsemantics is naturally nondeterministic . But more interestingly, the semantics is also not confluent .For one source of non-confluence, let us consider two reduction sequences of a same (closed)computation, where for better readability, we highlight the active redex for each reduction step: ↓ op ( 𝑉 , promise ( op 𝑥 ↦→ ( promise ( op ′ 𝑦 ↦→ 𝑀 ) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in 𝑀 ′ )) as 𝑝 in 𝑁 ) (cid:123) ↓ op ( 𝑉 , promise ( op 𝑥 ↦→ ( promise ( op ′ 𝑦 ↦→ 𝑀 ) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in 𝑀 ′ )) as 𝑝 in 𝑁 ′ ) (cid:123) let 𝑝 = ( promise ( op ′ 𝑦 ↦→ 𝑀 [ 𝑉 / 𝑥 ]) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in 𝑀 ′ ) in ↓ op ( 𝑉 , 𝑁 ′ ) (cid:123) promise ( op ′ 𝑦 ↦→ 𝑀 [ 𝑉 / 𝑥 ]) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in ( let 𝑝 = 𝑀 ′ in ↓ op ( 𝑉 , 𝑁 ′ )) and ↓ op ( 𝑉 , promise ( op 𝑥 ↦→ ( promise ( op ′ 𝑦 ↦→ 𝑀 ) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in 𝑀 ′ )) as 𝑝 in 𝑁 ) (cid:123) let 𝑝 = ( promise ( op ′ 𝑦 ↦→ 𝑀 [ 𝑉 / 𝑥 ]) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in 𝑀 ′ ) in ↓ op ( 𝑉 , 𝑁 ) (cid:123) promise ( op ′ 𝑦 ↦→ 𝑀 [ 𝑉 / 𝑥 ]) as 𝑞 in await 𝑞 until ⟨ 𝑧 ⟩ in ( let 𝑝 = 𝑀 ′ in ↓ op ( 𝑉 , 𝑁 )) Here, both final computations are temporarily blocked until an incoming interrupt op ′ is propagatedto them and the (promise) variable 𝑞 gets bound to a fulfilled promise. Until this happens, it is notpossible for the blocked continuation 𝑁 to reduce to 𝑁 ′ in the latter final computation. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:11
Ground type 𝐴 , 𝐵 :: = b (cid:12)(cid:12) (cid:12)(cid:12) (cid:12)(cid:12) 𝐴 × 𝐵 (cid:12)(cid:12) 𝐴 + 𝐵 Signal or interrupt signature: op : 𝐴 op Outgoing signal annotations: 𝑜 ∈ 𝑂 Interrupt handler annotations: 𝜄 ∈ 𝐼 Value type 𝑋 , 𝑌 :: = 𝐴 (cid:12)(cid:12) 𝑋 × 𝑌 (cid:12)(cid:12) 𝑋 + 𝑌 (cid:12)(cid:12) 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) (cid:12)(cid:12) ⟨ 𝑋 ⟩ Computation type: 𝑋 ! ( 𝑜, 𝜄 ) Fig. 3. Value and computation types
Another distinct source of non-confluence concerns the commutativity of outgoing signals withenveloping interrupt handlers. For instance, the following (closed) composite computation ↓ op ( 𝑉 , promise ( op 𝑥 ↦→ ↑ op ′ ( 𝑊 ′ , 𝑀 )) as 𝑝 in ↑ op ′′ ( 𝑊 ′′ , 𝑁 )) can nondeterministically reduce to either of the next two computations: ↑ op ′ ( 𝑊 ′ , ↑ op ′′ ( 𝑊 ′′ , let 𝑝 = 𝑀 in ↓ op ( 𝑉 , 𝑁 ))) ↑ op ′′ ( 𝑊 ′′ , ↑ op ′ ( 𝑊 ′ , let 𝑝 = 𝑀 in ↓ op ( 𝑉 , 𝑁 ))) depending on whether we first propagate the interrupt op inwards or the signal op ′′ outwards. Asa result, in the resulting two computations, the signals op ′ and op ′′ get issued in a different order. We equip 𝜆 æ with a type system in the tradition of type-and-effect systems for algebraic effects andeffect handlers [Bauer and Pretnar 2014; Kammar et al. 2013], by extending the simple type systemof FGCBV with annotations about possible effects in function and computation types. We define types in Figure 3, separated into ground, value, and computation types.As noted in Section 3.1, 𝜆 æ is parameterised over a set Σ of signal and interrupt names . To eachsuch name op ∈ Σ , we assign a fixed signature op : 𝐴 op that specifies the type 𝐴 op of the payloadof the corresponding signal or interrupt. Crucially, in order to be able to later prove that 𝜆 æ is typesafe (see Theorem 3.3, but also the relevant discussion in Section 6), we restrict these signatures to ground types 𝐴, 𝐵, . . . , which include standard base, unit, empty, product, and sum types.
Value types
𝑋, 𝑌, . . . extend ground types with function and promise types. The function type 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) classifies functions that take 𝑋 -typed arguments to computations classified by the computation type 𝑌 ! ( 𝑜, 𝜄 ) , i.e., ones that return 𝑌 -typed values, while possibly issuing signalsspecified by 𝑜 and handling interrupts specified by 𝜄 . The effect annotations 𝑜 and 𝜄 are drawn fromsets 𝑂 and 𝐼 whose definitions we discuss below in Section 3.3.2. Finally, the 𝜆 æ -specific promisetype ⟨ 𝑋 ⟩ classifies those promises that can be fulfilled by supplying a value of type 𝑋 . We now explain how we define the sets 𝑂 and 𝐼 from which we drawthe effect annotations that we use for specifying functions and computations. Traditionally, effectsystems for algebraic effects simply use (flat) sets of operation names for effect annotations [Bauerand Pretnar 2014; Kammar et al. 2013]. In 𝜆 æ , however, we need to be more careful, becausetriggering an interrupt handler executes a computation that can issue potentially different signalsand handle different interrupts from the main program, and we would like to capture this in types. Signal annotations.
First, as outgoing signals do not carry any computational data, we follow thetradition of type-and-effect systems for algebraic effects, and let 𝑂 be the power set P ( Σ ) . As such,each 𝑜 ∈ 𝑂 is a subset of the signature Σ , specifying which signals a computation might issue. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
Interrupt handler annotations.
As noted above, for specifying installed interrupt handlers, wecannot use (flat) sets of interrupt names as the effect annotations 𝜄 ∈ 𝐼 if we want to track thenested effectful structure. Instead, we define 𝐼 as the greatest fixed point of a set functor Φ given by Φ ( 𝑋 ) def = Σ ⇒ ( 𝑂 × 𝑋 ) ⊥ where ⇒ is exponentiation, × is Cartesian product, and (−) ⊥ is the lifting operation 𝑋 ⊥ def = 𝑋 ∪· {⊥} ,and where ∪· is the disjoint union of sets. Formally speaking, 𝐼 is given by an isomorphism 𝐼 (cid:27) Φ ( 𝐼 ) ,but for presentation purposes we leave it implicit and work as if we had a strict equality 𝐼 = Φ ( 𝐼 ) .Intuitively, each 𝜄 ∈ 𝐼 is a possibly infinite nesting of partial mappings of pairs of 𝑂 - and 𝐼 -annotations to names in Σ —these pairs of annotations classify the possible effects of the correspond-ing interrupt handler code. We use the record notation 𝜄 = { op ↦→ ( 𝑜 , 𝜄 ) , . . . , op 𝑛 ↦→ ( 𝑜 𝑛 , 𝜄 𝑛 )} to mean that 𝜄 maps the names op , . . . , op 𝑛 to the annotations ( 𝑜 , 𝜄 ) , . . . , ( 𝑜 𝑛 , 𝜄 𝑛 ) , and any othernames in Σ to ⊥ . We write 𝜄 ( op 𝑖 ) = ( 𝑜 𝑖 , 𝜄 𝑖 ) to mean that the annotation 𝜄 maps op 𝑖 to ( 𝑜 𝑖 , 𝜄 𝑖 ) . Subtyping and recursive effect annotations.
Both 𝑂 and 𝐼 come equipped with natural partialorders : for 𝑂 , ⊑ 𝑂 is given simply by subset inclusion; and for 𝐼 , ⊑ 𝐼 is characterised as follows: 𝜄 ⊑ 𝐼 𝜄 ′ iff ∀ ( op ∈ Σ ) ( 𝑜 ′′ ∈ 𝑂 ) ( 𝜄 ′′ ∈ 𝐼 ) . 𝜄 ( op ) = ( 𝑜 ′′ , 𝜄 ′′ ) = ⇒∃ ( 𝑜 ′′′ ∈ 𝑂 ) ( 𝜄 ′′′ ∈ 𝐼 ) . 𝜄 ′ ( op ) = ( 𝑜 ′′′ , 𝜄 ′′′ ) ∧ 𝑜 ′′ ⊑ 𝑂 𝑜 ′′′ ∧ 𝜄 ′′ ⊑ 𝐼 𝜄 ′′′ We often also use the product order ⊑ 𝑂 × 𝐼 , defined as ( 𝑜, 𝜄 ) ⊑ 𝑂 × 𝐼 ( 𝑜 ′ , 𝜄 ′ ) def = 𝑜 ⊑ 𝑂 𝑜 ′ ∧ 𝜄 ⊑ 𝐼 𝜄 ′ . Inparticular, we use ⊑ 𝑂 × 𝐼 in Section 3.3.3 to define the subtyping relation for 𝜆 æ ’s computation types.Importantly, the partial orders ( 𝑂, ⊑ 𝑂 ) and ( 𝐼, ⊑ 𝐼 ) are both 𝜔 -complete and pointed , i.e., they are pointed cpos , meaning they have least upper bounds of all increasing 𝜔 -chains, and least elements(given by the empty set ∅ and the constant ⊥ -valued mapping, respectively). As a result, least fixedpoints of continuous (endo)maps on them are guaranteed to exist. We refer the interested reader toAmadio and Curien [1998] and Gierz et al. [2003] for additional domain-theoretic background.For 𝜆 æ , we are particularly interested in the least fixed points of continuous maps 𝑓 : 𝐼 → 𝐼 , soas to specify and typecheck recursive interrupt handler examples, as we illustrate in Section 3.3.4.We also note that if we were only interested in the type safety of 𝜆 æ , and not in typecheckingrecursively defined interrupt handlers, then we would not need ( 𝐼, ⊑ 𝐼 ) to be 𝜔 -complete , and couldhave instead chosen 𝐼 to be the least fixed point of Φ , which is what we do in our Agda formalisation.In this case, each interrupt handler annotation 𝜄 ∈ 𝐼 would be a finite nesting of partial mappings .Finally, we envisage that any future full-fledged high-level language based on 𝜆 æ would allowusers to define their (recursive) effect annotations in a small domain-specific language, providing asyntactic counterpart to the domain-theoretic development we use for typing 𝜆 æ in this paper. We characterise well-typed values using the judgement Γ ⊢ 𝑉 : 𝑋 and well-typed computations using Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) . In both judgements, Γ is a typing context of the form 𝑥 : 𝑋 , . . . , 𝑥 𝑛 : 𝑋 𝑛 . The rules defining these judgements are respectively given in Figure 4 and 5. Values.
The rules for values are mostly standard. The only 𝜆 æ -specific rule is TyVal-Promise,which states that in order to fulfil a promise of type ⟨ 𝑋 ⟩ , one has to supply a value of type 𝑋 . Computations.
Analogously to values, the typing rules are standard for the computation termsthat 𝜆 æ inherits from FGCBV, with the 𝜆 æ -rules additionally tracking the effect information ( 𝑜, 𝜄 ) .The 𝜆 æ -specific rule TyComp-Signal states that in order to issue a signal op in a computationwith type 𝑋 ! ( 𝑜, 𝜄 ) , we must have op ∈ 𝑜 and the type of the payload has to match op ’s signature.The rule TyComp-Promise states that the interrupt handler code 𝑀 has to return a fulfilledpromise of type ⟨ 𝑋 ⟩ , for some type 𝑋 , while possibly issuing signals 𝑜 ′ and handling interrupts 𝜄 ′ , Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:13
TyVal-Var Γ , 𝑥 : 𝑋, Γ ′ ⊢ 𝑥 : 𝑋 TyVal-Unit Γ ⊢ () : TyVal-Pair Γ ⊢ 𝑉 : 𝑋 Γ ⊢ 𝑊 : 𝑌 Γ ⊢ ( 𝑉 ,𝑊 ) : 𝑋 × 𝑌 TyVal-Promise Γ ⊢ 𝑉 : 𝑋 Γ ⊢ ⟨ 𝑉 ⟩ : ⟨ 𝑋 ⟩ TyVal-Inl Γ ⊢ 𝑉 : 𝑋 Γ ⊢ inl 𝑌 𝑉 : 𝑋 + 𝑌 TyVal-Inr Γ ⊢ 𝑊 : 𝑌 Γ ⊢ inr 𝑋 𝑊 : 𝑋 + 𝑌 TyVal-Fun Γ , 𝑥 : 𝑋 ⊢ 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) Γ ⊢ fun ( 𝑥 : 𝑋 ) ↦→ 𝑀 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) Fig. 4. Value typing rules.
TyComp-Return Γ ⊢ 𝑉 : 𝑋 Γ ⊢ return 𝑉 : 𝑋 ! ( 𝑜, 𝜄 ) TyComp-Let Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) Γ , 𝑥 : 𝑋 ⊢ 𝑁 : 𝑌 ! ( 𝑜, 𝜄 ) Γ ⊢ let 𝑥 = 𝑀 in 𝑁 : 𝑌 ! ( 𝑜, 𝜄 ) TyComp-LetRec Γ , 𝑓 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) , 𝑥 : 𝑋 ⊢ 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) Γ , 𝑓 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) ⊢ 𝑁 : 𝑍 ! ( 𝑜 ′ , 𝜄 ′ ) Γ ⊢ let rec 𝑓 𝑥 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) = 𝑀 in 𝑁 : 𝑍 ! ( 𝑜 ′ , 𝜄 ′ ) TyComp-Apply Γ ⊢ 𝑉 : 𝑋 → 𝑌 ! ( 𝑜, 𝜄 ) Γ ⊢ 𝑊 : 𝑋 Γ ⊢ 𝑉 𝑊 : 𝑌 ! ( 𝑜, 𝜄 ) TyComp-MatchPair Γ ⊢ 𝑉 : 𝑋 × 𝑌 Γ , 𝑥 : 𝑋, 𝑦 : 𝑌 ⊢ 𝑀 : 𝑍 ! ( 𝑜, 𝜄 ) Γ ⊢ match 𝑉 with {( 𝑥, 𝑦 ) ↦→ 𝑀 } : 𝑍 ! ( 𝑜, 𝜄 ) TyComp-MatchEmpty Γ ⊢ 𝑉 : Γ ⊢ match 𝑉 with {} 𝑍 ! ( 𝑜,𝜄 ) : 𝑍 ! ( 𝑜, 𝜄 ) TyComp-MatchSum Γ ⊢ 𝑉 : 𝑋 + 𝑌 Γ , 𝑥 : 𝑋 ⊢ 𝑀 : 𝑍 ! ( 𝑜, 𝜄 ) Γ , 𝑦 : 𝑌 ⊢ 𝑁 : 𝑍 ! ( 𝑜, 𝜄 ) Γ ⊢ match 𝑉 with { inl 𝑥 ↦→ 𝑀, inr 𝑦 ↦→ 𝑁 } : 𝑍 ! ( 𝑜, 𝜄 ) TyComp-Signal op ∈ 𝑜 Γ ⊢ 𝑉 : 𝐴 op Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) Γ ⊢ ↑ op ( 𝑉 , 𝑀 ) : 𝑋 ! ( 𝑜, 𝜄 ) TyComp-Interrupt Γ ⊢ 𝑉 : 𝐴 op Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) Γ ⊢ ↓ op ( 𝑉 , 𝑀 ) : 𝑋 ! op ↓ ( 𝑜, 𝜄 ) TyComp-Promise 𝜄 ( op ) = ( 𝑜 ′ , 𝜄 ′ ) Γ , 𝑥 : 𝐴 op ⊢ 𝑀 : ⟨ 𝑋 ⟩ ! ( 𝑜 ′ , 𝜄 ′ ) Γ , 𝑝 : ⟨ 𝑋 ⟩ ⊢ 𝑁 : 𝑌 ! ( 𝑜, 𝜄 ) Γ ⊢ promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 : 𝑌 ! ( 𝑜, 𝜄 ) TyComp-Await Γ ⊢ 𝑉 : ⟨ 𝑋 ⟩ Γ , 𝑥 : 𝑋 ⊢ 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) Γ ⊢ await 𝑉 until ⟨ 𝑥 ⟩ in 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) TyComp-Subsume Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) ( 𝑜, 𝜄 ) ⊑ 𝑂 × 𝐼 ( 𝑜 ′ , 𝜄 ′ ) Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜 ′ , 𝜄 ′ ) Fig. 5. Computation typing rules. both of which are determined by the effect annotation 𝜄 of the entire computation, i.e., 𝜄 ( op ) = ( 𝑜 ′ , 𝜄 ′ ) .The variable 𝑝 bound in the continuation, which sub-computations can block on to await a giveninterrupt to arrive and be handled, also gets assigned the promise type ⟨ 𝑋 ⟩ . It is worth noting thatone could have had 𝑀 simply return values of type 𝑋 , but at the cost of not being able to implementsome of the more interesting examples, e.g., guarded interrupt handlers in Section 5.1. At the sametime, for 𝜆 æ ’s type safety, it is crucial that 𝑝 would have remained assigned the promise type ⟨ 𝑋 ⟩ .The rule TyComp-Await simply says that when awaiting a promise of type ⟨ 𝑋 ⟩ to be fulfilled,the continuation 𝑀 can refer to the promised value (in the future) using the variable 𝑥 of type 𝑋 . Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
The rule TyComp-Interrupt is used to type incoming interrupts. In particular, when the outsideworld propagates an interrupt op to a computation 𝑀 of type 𝑋 ! ( 𝑜, 𝜄 ) , the resulting computation ↓ op ( 𝑉 , 𝑀 ) gets assigned the type 𝑋 ! op ↓ ( 𝑜, 𝜄 ) , where the interrupt op also acts on the effectannotations. Intuitively, op ↓ ( 𝑜, 𝜄 ) mimics the act of triggering interrupt handlers for op at the levelof effect annotations. Formally, we define this action of interrupts on effect annotations as follows: op ↓ ( 𝑜, 𝜄 ) def = (cid:40) ( 𝑜 ∪ 𝑜 ′ , 𝜄 [ op ↦→ ⊥] ∪ 𝜄 ′ ) if 𝜄 ( op ) = ( 𝑜 ′ , 𝜄 ′ )( 𝑜, 𝜄 ) otherwiseIn other words, if 𝑀 has any interrupt handlers installed for op , then 𝜄 ( op ) = ( 𝑜 ′ , 𝜄 ′ ) , where ( 𝑜 ′ , 𝜄 ′ ) specifies the effects of said interrupt handler code. Now, when the inward propagating interrupt op reaches those interrupt handlers, it triggers the execution of the corresponding handler code, andthus the entire computation ↓ op ( 𝑉 , 𝑀 ) can also issue signals in 𝑜 ′ and handle interrupts in 𝜄 ′ .The notation 𝜄 [ op ↦→ ⊥] sets 𝜄 to ⊥ at op , and leaves it unchanged elsewhere. In particular,mapping op to ⊥ captures that the interrupt triggers all corresponding interrupt handlers in 𝑀 .The join-semilattice structure 𝑜 ∪ 𝑜 ′ ∈ 𝑂 is given by the union of sets, while 𝜄 ∪ 𝜄 ′ ∈ 𝐼 is given by 𝜄 ∪ 𝜄 ′ def = 𝜆 op . ( 𝑜 ′′ ∪ 𝑜 ′′′ , 𝜄 ′′ ∪ 𝜄 ′′′ ) if 𝜄 ( op ) = ( 𝑜 ′′ , 𝜄 ′′ ) ∧ 𝜄 ′ ( op ) = ( 𝑜 ′′′ , 𝜄 ′′′ )( 𝑜 ′′ , 𝜄 ′′ ) if 𝜄 ( op ) = ( 𝑜 ′′ , 𝜄 ′′ ) ∧ 𝜄 ′ ( op ) = ⊥( 𝑜 ′′′ , 𝜄 ′′′ ) if 𝜄 ( op ) = ⊥ ∧ 𝜄 ′ ( op ) = ( 𝑜 ′′′ , 𝜄 ′′′ )⊥ if 𝜄 ( op ) = ⊥ ∧ 𝜄 ′ ( op ) = ⊥ We also note that the action op ↓ (−) has various useful properties, which we use in later proofs(where we write 𝜋 and 𝜋 for the two projections associated with the Cartesian product 𝑂 × 𝐼 ):Lemma 3.1.(1) 𝑜 ⊑ 𝑂 𝜋 ( op ↓ ( 𝑜, 𝜄 )) . (2) If 𝜄 ( op ) = ( 𝑜 ′ , 𝜄 ′ ) , then ( 𝑜 ′ , 𝜄 ′ ) ⊑ 𝑂 × 𝐼 op ↓ ( 𝑜, 𝜄 ) . (3) If op ≠ op ′ and 𝜄 ( op ′ ) = ( 𝑜 ′ , 𝜄 ′ ) , then ( 𝑜 ′ , 𝜄 ′ ) ⊑ 𝑂 × 𝐼 ( 𝜋 ( op ↓ ( 𝑜, 𝜄 ))) ( op ′ ) . Finally, the rule TyComp-Subsume allows subtyping . To simplify the presentation, we consider alimited form of subtyping, in which we shallowly relate only signal and interrupt annotations.
We conclude discussing 𝜆 æ ’s type-and-effect system by briefly returning to the reason why we defined our effect annotations usinglightweight domain theory in the first place, namely, so as to typecheck recursive interrupt handlers.As an example, we recall the following fragment of the server code from Section 2.6.2: let rec waitForBatchSize () =promise (batchSizeReq () ↦→ ↑ batchSizeResp batchSize ; waitForBatchSize ()) as p in return p Here, waitForBatchSize () is an interrupt handler for batchSizeReq that recursively reinstalls itselfimmediately after issuing a batchSizeResp signal. Due to its recursive definition, it is not surprisingthat the type of waitForBatchSize should also be given recursively, in particular, if we want to giveit a more precise type than one which simply says that any effect is possible.To this end, we assign waitForBatchSize the type → ⟨ ⟩ ! (∅ , 𝜄 b ) , where 𝜄 b is the least fixed point of the continuous map 𝜄 ↦→ { batchSizeReq ↦→ ({ batchSizeResp } , 𝜄 ) } : 𝐼 → 𝐼 , i.e., 𝜄 b = (cid:8) batchSizeReq ↦→ ({ batchSizeResp } , { batchSizeReq ↦→ ({ batchSizeResp } , . . . ) }) (cid:9) As such, (∅ , 𝜄 b ) captures that at the top level waitForBatchSize () installs an interrupt handler andissues no signals, and that every batchSizeReq interrupt causes a signal to be issued and theinterrupt handler to be reinstalled. Checking that waitForBatchSize has the type → ⟨ ⟩ ! (∅ , 𝜄 b ) Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:15 involves unfolding the definition of 𝜄 b and using subtyping. The latter is needed when we recursivelycall waitForBatchSize () where a computation of type ⟨ ⟩ ! ({ batchSizeResp } , 𝜄 b ) is expected. We now prove type safety for the sequential part of 𝜆 æ , showing that “well-typed programs do notgo wrong”. As usual, we split type safety into progress and preservation [Wright and Felleisen 1994]. The progress result says that well-typed closed computations can either make astep of reduction, or are already in a well-defined result form (and thus have stopped reducing).As such, we first need to define when we consider 𝜆 æ -computations to be in result form. It isimportant to note that for 𝜆 æ , the result forms have to also incorporate the temporary blocking while computations await some promise (variable) 𝑝 to be fulfilled. Therefore, as a first step, wecharacterise such computations using the judgement 𝑝 ⊲⊳ 𝑀 , given by the following three rules: 𝑝 ⊲⊳ await 𝑝 until ⟨ 𝑥 ⟩ in 𝑀 𝑝 ⊲⊳ 𝑀𝑝 ⊲⊳ let 𝑥 = 𝑀 in 𝑁 𝑝 ⊲⊳ 𝑀𝑝 ⊲⊳ ↓ op ( 𝑉 , 𝑀 ) Next, we characterise 𝜆 æ ’s result forms using the judgements CompRes ⟨ Ψ | 𝑀 ⟩ and RunRes ⟨ Ψ | 𝑀 ⟩ : CompRes ⟨ Ψ | 𝑀 ⟩ CompRes ⟨ Ψ | ↑ op ( 𝑉 , 𝑀 )⟩ RunRes ⟨ Ψ | 𝑀 ⟩ CompRes ⟨ Ψ | 𝑀 ⟩ RunRes ⟨ Ψ | return 𝑉 ⟩ RunRes ⟨ Ψ ∪ { 𝑝 } | 𝑁 ⟩ RunRes ⟨ Ψ | promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ⟩ 𝑝 ∈ Ψ 𝑝 ⊲⊳ 𝑀 RunRes ⟨ Ψ | 𝑀 ⟩ In these judgements, Ψ is a set of (promise) variables that have been bound by interrupt handlersenveloping the computation. These judgements express that a computation 𝑀 is in a (top-level)result form CompRes ⟨ Ψ | 𝑀 ⟩ when, considered as a tree, it has a shape in which all signals aretowards the root, interrupt handlers are in the intermediate nodes, and the leaves contain returnvalues and computations that are temporarily blocked while awaiting one of the promise variablesin Ψ to be fulfilled. The slightly mysterious name of the intermediate judgement RunRes ⟨ Ψ | 𝑀 ⟩ will become clear in Section 4.4. The finality of these result forms is captured by the next lemma.Lemma 3.2. Given Ψ and 𝑀 such that CompRes ⟨ Ψ | 𝑀 ⟩ , then there exists no 𝑁 with 𝑀 (cid:123) 𝑁 . We are now ready to state and prove the progress theorem for the sequential part of 𝜆 æ .Theorem 3.3. Given a well-typed computation Γ ⊢ 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) , where Γ = 𝑥 : ⟨ 𝑋 ⟩ , . . . , 𝑥 𝑛 : ⟨ 𝑋 𝑛 ⟩ ,then either (i) there exists an 𝑁 such that 𝑀 (cid:123) 𝑁 , or (ii) we have CompRes ⟨{ 𝑥 , . . . , 𝑥 𝑛 } | 𝑀 ⟩ . Proof. The proof is standard and proceeds by induction on the derivation of Γ ⊢ 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) .For instance, if the derivation ends with a typing rule for function application or pattern-matching,we use an auxiliary canonical forms lemma to show that the value involved is either a functionabstraction or in constructor form—thus 𝑀 can 𝛽 -reduce and we prove (i). Here we crucially rely onthe context Γ having the specific assumed form 𝑥 : ⟨ 𝑋 ⟩ , . . . , 𝑥 𝑛 : ⟨ 𝑋 𝑛 ⟩ . If the derivation ends withTyComp-Await, then we use a canonical forms lemma to show that the promise value is either avariable in Γ , in which case we prove (ii), or in constructor form, in which case we prove (i). If thederivation however ends with a typing rule for any of the terms figuring in the evaluation contexts E , then we proceed based on using the induction hypothesis on the corresponding continuation. □ Corollary 3.4.
Given a well-typed closed computation ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) , then either (i) there exists acomputation 𝑁 such that 𝑀 (cid:123) 𝑁 , or (ii) 𝑀 is already in result form, i.e., we have CompRes ⟨∅ | 𝑀 ⟩ . Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
The type preservation result says that reduction preserves well-typedness.The results that we present in this section use standard substitution lemmas . For instance, given Γ , 𝑥 : 𝑋, Γ ′ ⊢ 𝑀 : 𝑌 ! ( 𝑜, 𝜄 ) and Γ ⊢ 𝑉 : 𝑋 , then we can show that Γ , Γ ′ ⊢ 𝑀 [ 𝑉 / 𝑥 ] : 𝑌 ! ( 𝑜, 𝜄 ) . In thefollowing we also use standard typing inversion lemmas . For example, given Γ ⊢ ↓ op ( 𝑉 , 𝑀 ) : 𝑋 ! ( 𝑜, 𝜄 ) ,then we can show that Γ ⊢ 𝑉 : 𝐴 op and Γ ⊢ 𝑀 : 𝑋 ! op ↓ ( 𝑜 ′ , 𝜄 ′ ) , such that op ↓ ( 𝑜 ′ , 𝜄 ′ ) ⊑ 𝑂 × 𝐼 ( 𝑜, 𝜄 ) .As the proof of type preservation proceeds by induction on reduction steps, we find it useful todefine an auxiliary typing judgement for evaluation contexts , written Γ ⊢[ Γ ′ | 𝑋 ! ( 𝑜, 𝜄 ) ] E : 𝑌 ! ( 𝑜 ′ , 𝜄 ′ ) ,which we then use to prove the evaluation context rule case of the proof. Here, Γ ′ is the contextof variables bound by the interrupt handlers in E , and 𝑋 ! ( 𝑜, 𝜄 ) is the type of the hole [ ] . Thisjudgement is given using rules similar to those for computations, including subtyping, e.g., we have 𝜄 ′ ( op ) = ( 𝑜 ′′ , 𝜄 ′′ ) Γ , 𝑥 : 𝐴 op ⊢ 𝑀 : ⟨ 𝑌 ⟩ ! ( 𝑜 ′′ , 𝜄 ′′ ) Γ , 𝑝 : ⟨ 𝑌 ⟩ ⊢[ Γ ′ | 𝑋 ! ( 𝑜, 𝜄 ) ] E : 𝑍 ! ( 𝑜 ′ , 𝜄 ′ ) Γ ⊢[ 𝑝 : ⟨ 𝑌 ⟩ , Γ ′ | 𝑋 ! ( 𝑜, 𝜄 ) ] promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in E : 𝑍 ! ( 𝑜 ′ , 𝜄 ′ ) It is thus straightforward to relate this typing of evaluation contexts with that of computations.Lemma 3.5. Γ ⊢ E [ 𝑀 ] : 𝑌 ! ( 𝑜 ′ , 𝜄 ′ ) ⇔ ∃ Γ ′ , 𝑋, 𝑜, 𝜄. Γ ⊢[ Γ ′ | 𝑋 ! ( 𝑜, 𝜄 ) ] E : 𝑌 ! ( 𝑜 ′ , 𝜄 ′ ) ∧ Γ , Γ ′ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) . We are now ready to state and prove the type preservation theorem for the sequential part of 𝜆 æ .Theorem 3.6. Given Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) and 𝑀 (cid:123) 𝑁 , then we have Γ ⊢ 𝑁 : 𝑋 ! ( 𝑜, 𝜄 ) . Proof. The proof is standard and proceeds by induction on the derivation of 𝑀 (cid:123) 𝑁 , usingtyping inversion lemmas depending on the structure forced upon 𝑀 by the last rule used in 𝑀 (cid:123) 𝑁 .There are four cases of interest in this proof. The first two concern the interaction of incominginterrupts and interrupt handlers. On the one hand, if the given derivation of (cid:123) ends with ↓ op ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) (cid:123) let 𝑝 = 𝑀 [ 𝑉 / 𝑥 ] in ↓ op ( 𝑉 , 𝑁 ) then in order to type the right-hand side of this rule, we are led to use subtyping with Lemma 3.1 (2),so as to show that 𝑀 ’s effect information is included in op ↓ ( 𝑜, 𝜄 ) . On the other hand, given ↓ op ′ ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) (cid:123) promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in ↓ op ′ ( 𝑉 , 𝑁 ) ( op ≠ op ′ ) then in order to type the right-hand side of this rule, we are led to use subtyping with Lemma 3.1 (3),so as to show that after acting on ( 𝑜, 𝜄 ) with op ′ , op remains mapped to 𝑀 ’s effect information.The third case of interest concerns the commutativity of signals with interrupt handlers: promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in ↑ op ′ ( 𝑉 , 𝑁 ) (cid:123) ↑ op ′ ( 𝑉 , promise ( op 𝑥 ↦→ 𝑀 ) as 𝑝 in 𝑁 ) where in order to type the signal’s payload 𝑉 in the right-hand side, it is crucial that the promise-typed variable 𝑝 cannot appear in 𝑉 —this is ensured by our type system that restricts the signatures op : 𝐴 op to ground types. As a result, we can strengthen the typing context of 𝑉 by removing 𝑝 .Finally, in the evaluation context rule case, we use the induction hypothesis with Lemma 3.5. □ Interestingly, the proof of Theorem 3.6 tells us that if one were to consider a variant of 𝜆 æ inwhich the TyComp-Subsume rule appeared as an explicit coercion term coerce ( 𝑜,𝜄 ) ⊑ 𝑂 × 𝐼 ( 𝑜 ′ ,𝜄 ′ ) 𝑀 , thenthe right-hand sides of the two interrupt propagation rules highlighted in the above proof wouldalso need to involve such coercions, corresponding to the uses of Lemma 3.1. This however meansthat other computations involved in these reduction rules would also need to be type-annotated. Next, we describe the parallel part of 𝜆 æ . Similarly to the sequential part, we again present thecorresponding syntax, a small-step semantics, a type-and-effect system, and type safety results. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:17
Individual computations 𝑀 (cid:123) 𝑁 run 𝑀 (cid:123) run 𝑁 Signal hoisting run (↑ op ( 𝑉 , 𝑀 )) (cid:123) ↑ op ( 𝑉 , run 𝑀 ) Broadcasting ↑ op ( 𝑉 , 𝑃 ) || 𝑄 (cid:123) ↑ op ( 𝑉 , 𝑃 || ↓ op ( 𝑉 , 𝑄 )) 𝑃 || ↑ op ( 𝑉 , 𝑄 ) (cid:123) ↑ op ( 𝑉 , ↓ op ( 𝑉 , 𝑃 ) || 𝑄 ) Interrupt propagation ↓ op ( 𝑉 , run 𝑀 ) (cid:123) run (↓ op ( 𝑉 , 𝑀 ))↓ op ( 𝑉 , 𝑃 || 𝑄 ) (cid:123) ↓ op ( 𝑉 , 𝑃 ) || ↓ op ( 𝑉 , 𝑄 )↓ op ( 𝑉 , ↑ op ′ ( 𝑊 , 𝑃 )) (cid:123) ↑ op ′ ( 𝑊 , ↓ op ( 𝑉 , 𝑃 )) Evaluation context rule 𝑃 (cid:123) 𝑄 F [ 𝑃 ] (cid:123) F [ 𝑄 ] where F :: = [ ] (cid:12)(cid:12) F || 𝑄 (cid:12)(cid:12) 𝑃 || F (cid:12)(cid:12) ↑ op ( 𝑉 ,
F ) (cid:12)(cid:12) ↓ op ( 𝑉 ,
F )
Fig. 6. Small-step operational semantics of parallel processes.
To keep the presentation focussed on the asynchronous use of algebraic effects, we considera very simple model of parallelism: a process is either an individual computation or the parallelcomposition of two processes. To facilitate interactions between processes, they also contain outwardpropagating signals and inward propagating interrupts . Formally, the syntax of parallel processes is 𝑃, 𝑄 :: = run 𝑀 (cid:12)(cid:12) 𝑃 || 𝑄 (cid:12)(cid:12) ↑ op ( 𝑉 , 𝑃 ) (cid:12)(cid:12) ↓ op ( 𝑉 , 𝑃 ) Note that processes do not include interrupt handlers—these are local to individual computations.We leave first-class processes and their dynamic creation for future work, as discussed in Section 6.
We equip the parallel part of 𝜆 æ with a small-step semantics that naturally extends that of 𝜆 æ ’ssequential part. The semantics is defined using a reduction relation 𝑃 (cid:123) 𝑄 , as given in Figure 6. Individual computations.
This reduction rule states that, as processes, individual computationsevolve according to the small-step operational semantics 𝑀 (cid:123) 𝑁 we defined for them in Section 3.2. Signal hoisting.
This rule propagates signals out of individual computations. It is important tonote that we only hoist those signals that have propagated to the outer boundary of a computation.
Broadcasting.
The broadcast rules turn outward moving signals in one process into inwardmoving interrupts for the process parallel to it, while continuing to propagate the signals outwardsto any further parallel processes. The latter ensures that the semantics is compositional.
Interrupt propagation.
These three rules simply propagate interrupts inwards into individualcomputations, into all branches of parallel compositions, and past any outward moving signals.
Evaluation contexts.
Analogously to the semantics of computations, the semantics of processesalso includes a context rule, which allows reductions under evaluation contexts F . Observe thatcompared to the evaluation contexts for computations, those for processes do not bind variables. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
TyProc-Run Γ ⊢ 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) Γ ⊢ run 𝑀 : 𝑋 !! ( 𝑜, 𝜄 ) TyProc-Par Γ ⊢ 𝑃 : 𝐶 Γ ⊢ 𝑄 : 𝐷 Γ ⊢ 𝑃 || 𝑄 : 𝐶 || 𝐷 TyProc-Signal op ∈ signals - of ( 𝐶 ) Γ ⊢ 𝑉 : 𝐴 op Γ ⊢ 𝑃 : 𝐶 Γ ⊢ ↑ op ( 𝑉 , 𝑃 ) : 𝐶 TyProc-Interrupt Γ ⊢ 𝑉 : 𝐴 op Γ ⊢ 𝑃 : 𝐶 Γ ⊢ ↓ op ( 𝑉 , 𝑃 ) : op ↓ 𝐶 Fig. 7. Process typing rules.
Analogously to its sequential part, we also equip 𝜆 æ ’s parallel part with a type-and-effect system. Types.
The types of processes are designed to match their parallel structure—they are given by 𝐶 , 𝐷 :: = 𝑋 !! ( 𝑜, 𝜄 ) (cid:12)(cid:12) 𝐶 || 𝐷 Intuitively, 𝑋 !! ( 𝑜, 𝜄 ) is a process type of an individual computation of type 𝑋 ! ( 𝑜, 𝜄 ) , and 𝐶 || 𝐷 isthe type of the parallel composition of two processes that respectively have types 𝐶 and 𝐷 . Typing judgements. Well-typed processes are characterised using the judgement Γ ⊢ 𝑃 : 𝐶 . Wepresent the typing rules in Figure 7. While our processes are not currently higher-order, we allownon-empty contexts Γ to model the possibility of using libraries and top-level function definitions.The rules TyProc-Run and TyProc-Par capture the earlier intuition about the types of processesmatching their parallel structure. The rules TyProc-Signal and TyProc-Interrupt are similar tothe corresponding rules from Figure 5. The signal annotations of a process type are calculated as signals - of ( 𝑋 !! ( 𝑜, 𝜄 )) def = 𝑜 signals - of ( 𝐶 || 𝐷 ) def = signals - of ( 𝐶 ) ∪ signals - of ( 𝐷 ) and the action of interrupts on process types op ↓ 𝐶 extends the action on effect annotations as op ↓ ( 𝑋 !! ( 𝑜, 𝜄 )) def = 𝑋 !! ( op ↓ ( 𝑜, 𝜄 )) op ↓ ( 𝐶 || 𝐷 ) def = ( op ↓ 𝐶 ) || ( op ↓ 𝐷 ) by propagating the interrupt towards the types of individual computations. We then have:Lemma 4.1. For any process type 𝐶 and interrupt op , we have that signals - of ( 𝐶 ) ⊑ 𝑂 𝜋 ( op ↓ 𝐶 ) . It is worth noting that Figure 7 does not include an analogue of TyComp-Subsume. This isdeliberate because as we shall see below, process types reduce in conjunction with the processesthey are assigned to, and the outcome is generally neither a sub- nor supertype of the original type.
We conclude the meta-theory of 𝜆 æ by proving type safety for its parallel part. Analogously toSection 3.4, we once again split type safety into separate proofs of progress and preservation . We characterise the result forms of parallel processes by defining two judgements,
ProcRes ⟨ 𝑃 ⟩ and ParRes ⟨ 𝑃 ⟩ , and by using the judgement RunRes ⟨ Ψ | 𝑀 ⟩ from Section 3.4, as follows: ProcRes ⟨ 𝑃 ⟩ ProcRes ⟨↑ op ( 𝑉 , 𝑃 )⟩ ParRes ⟨ 𝑃 ⟩ ProcRes ⟨ 𝑃 ⟩ RunRes ⟨∅ | 𝑀 ⟩ ParRes ⟨ run 𝑀 ⟩ ParRes ⟨ 𝑃 ⟩ ParRes ⟨ 𝑄 ⟩ ParRes ⟨ 𝑃 || 𝑄 ⟩ These judgements express that a process 𝑃 is in a (top-level) result form ProcRes ⟨ 𝑃 ⟩ when, consid-ered as a tree, it has a shape in which all signals are towards the root, parallel compositions arein the intermediate nodes, and individual computation results are at the leaves. Importantly, thecomputation results RunRes ⟨∅ | 𝑀 ⟩ we use here are those from which signals have been propagatedout of (see Section 3.4.1). The finality of these results forms is then captured by the next lemma. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:19
Lemma 4.2.
Given a process 𝑃 such that ProcRes ⟨ 𝑃 ⟩ , then there exists no 𝑄 such that 𝑃 (cid:123) 𝑄 . We are now ready to state and prove the progress theorem for the parallel part of 𝜆 æ .Theorem 4.3. Given a well-typed closed process ⊢ 𝑃 : 𝐶 , then either (i) there exists a process 𝑄 suchthat 𝑃 (cid:123) 𝑄 , or (ii) the process 𝑃 is already in a (top-level) result form, i.e., we have ProcRes ⟨ 𝑃 ⟩ . Proof. The proof is standard and proceeds by induction on the derivation of ⊢ 𝑃 : 𝐶 . In the basecase, when the derivation ends with the TyProc-Run rule, and 𝑃 = run 𝑀 , we use Corollary 3.4. □ First, we note that the broadcast rules in Figure 6 introduce new inwardpropagating interrupts in their right-hand sides that originally do not exist in their left-hand sides.As a result, compared to the types one assigns to the left-hand sides of these reduction rules, thetypes assigned to their right-hand sides will need to feature corresponding type-level actions ofthese interrupts. We formalise this idea using a process type reduction relation 𝐶 ⇝ 𝐷 , given by 𝑋 !! ( 𝑜, 𝜄 ) ⇝ 𝑋 !! ( 𝑜, 𝜄 ) 𝑋 !! ops ↓↓ ( 𝑜, 𝜄 ) ⇝ 𝑋 !! ops ↓↓ ( op ↓ ( 𝑜, 𝜄 )) 𝐶 ⇝ 𝐶 ′ 𝐷 ⇝ 𝐷 ′ 𝐶 || 𝐷 ⇝ 𝐶 ′ || 𝐷 ′ where we write ops ↓↓ ( 𝑜, 𝜄 ) for a recursively defined action of a list of interrupts on ( 𝑜, 𝜄 ) , given by [] ↓↓ ( 𝑜, 𝜄 ) def = ( 𝑜, 𝜄 ) ( op :: ops ) ↓↓ ( 𝑜, 𝜄 ) def = op ↓ ( ops ↓↓ ( 𝑜, 𝜄 )) Intuitively, 𝐶 ⇝ 𝐷 describes how process types reduce by being acted upon by freshly arrivinginterrupts. While we define the action behaviour only at the leaves of process types (under someenveloping sequence of actions), we can prove expected properties for arbitrary process types:Lemma 4.4.(1) Process types can remain unreduced, i.e., 𝐶 ⇝ 𝐶 for any process type 𝐶 . (2) Process types reduce by being acted upon, i.e., 𝐶 ⇝ op ↓ 𝐶 for any type 𝐶 and interrupt op . (3) Process types can reduce under enveloping actions, i.e., op ↓ 𝐶 ⇝ op ↓ 𝐷 when 𝐶 ⇝ 𝐷 . (4) Process type reduction can introduce signals, i.e., signals - of ( 𝐶 ) ⊑ 𝑂 signals - of ( 𝐷 ) when 𝐶 ⇝ 𝐷 . For the proof of Lemma 4.4 (3), it is important that we introduce interrupts under an arbitraryenveloping sequence of interrupt actions, and not simply as 𝑋 !! ( 𝑜, 𝜄 ) ⇝ 𝑋 !! ( op ↓ ( 𝑜, 𝜄 )) . Further,the proof of Lemma 4.4 (4) requires us to generalise Lemma 3.1 (1) to lists of enveloping actions:Lemma 4.5. 𝜋 ( ops ↓↓ ( 𝑜, 𝜄 )) ⊑ 𝑂 𝜋 ( ops ↓↓ ( op ↓ ( 𝑜, 𝜄 ))) As in Section 3.4.2, we again find it useful to define a separate typing judgement for evaluationcontexts , this time written Γ ⊢[ 𝐶 ] F : 𝐷 , together with an analogue of Lemma 3.5, which we omithere. Instead, we observe that this typing judgement is subject to process type reduction:Lemma 4.6. Given Γ ⊢[ 𝐶 ] F : 𝐷 and 𝐶 ⇝ 𝐶 ′ , then there exists 𝐷 ′ with 𝐷 ⇝ 𝐷 ′ and Γ ⊢[ 𝐶 ′ ] F : 𝐷 ′ . We are now ready to state and prove the type preservation theorem for the parallel part of 𝜆 æ .Theorem 4.7. Given a well-typed process Γ ⊢ 𝑃 : 𝐶 , such that 𝑃 can reduce as 𝑃 (cid:123) 𝑄 , then thereexists a process type 𝐷 , such that the process type 𝐶 can reduce as 𝐶 ⇝ 𝐷 , and we have Γ ⊢ 𝑄 : 𝐷 . Proof. The proof proceeds by induction on the derivation of 𝑃 (cid:123) 𝑄 , using auxiliary typinginversion lemmas depending on the structure forced upon 𝑃 by the last rule used in 𝑃 (cid:123) 𝑄 . For allbut the broadcast and evaluation context rules, we can pick 𝐷 to be 𝐶 and use Lemma 4.4 (1). Forthe broadcast rules, we define 𝐷 by introducing the corresponding interrupt, and build 𝐶 ⇝ 𝐷 using the parallel composition rule together with Lemma 4.4 (2). For the evaluation context rule,we use Lemma 4.6 in combination with the induction hypothesis. Finally, in order to dischargeeffects-related side-conditions when commuting interrupts with signals, we use Lemma 4.1. □ Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
We now show some examples of the kinds of programs one can write in 𝜆 æ . Similarly to Section 2.6,we again allow ourselves access to mutable references, and use generic versions ↑ op 𝑉 of signals. Before diving into the examples, we note that we often want the triggering of interrupt handlersto be based on not only the names of interrupts, but also the payloads that they carry. In order toexpress such more fine-grained triggering behaviour, we shall use a guarded interrupt handler : promise (op x when guard ↦→ comp) as p in cont which is simply a syntactic sugar for the following interrupt handler that recursively reinstallsitself until the boolean guard becomes true, in which case it executes the handler code comp : let rec waitForGuard () =promise (op x ↦→ if guard then comp else waitForGuard ()) as p' in return p'inlet p = waitForGuard () in cont Here, x is bound both in guard and comp . Further, if comp has type ⟨ 𝑋 ⟩ ! ( 𝑜 ′ , 𝜄 ′ ) and cont has type 𝑌 ! ( 𝑜, 𝜄 ) , such that 𝜄 ( op ) = ( 𝑜 ′ , 𝜄 ′ ) , then we can assign the entire computation the type 𝑌 ! ( 𝑜, 𝜄 ∪ 𝜄 ℎ ) ,where the effect annotation 𝜄 ℎ is the least fixed point of the map 𝜄 ′′ ↦→ { op ↦→ ( 𝑜 ′ , 𝜄 ′ ∪ 𝜄 ′′ )} : 𝐼 → 𝐼 .Observe that some of the recursive encoding leaks into the type of the entire computation via 𝜄 ℎ .Note that regardless whether guard is true, every interrupt is propagated into cont . To typechecktheir definition, and to ensure that guarded interrupt handlers are non-blocking, it is crucial that thehandler code of ordinary interrupt handlers returns promise-typed values, as noted in Section 3.3.3. Multi-threading remains one of the most exciting applications of algebraic effects, with the possibil-ity of expressing many evaluation strategies being the main reason for the extension of MulticoreOCaml with effect handlers [Dolan et al. 2018]. These evaluation strategies are however cooperative in nature, where each thread needs to explicitly yield back control, stalling other threads until then.While it is possible to also simulate preemptive multi-threading within the conventional treatmentof algebraic effects, it requires a low-level access to the specific runtime environment, so as toinject yields into the currently running computation [Dolan et al. 2018]. In contrast, implementingpreemptive multi-threading in 𝜆 æ is quite straightforward, and importantly, possible within thelanguage itself—the injections into the running computation take the form of incoming interrupts.For this, let us consider two interrupts, stop : and go : , that communicate to a thread whetherto pause or resume execution. These interrupts can originate from a timer process we run in parallel.At the core of our implementation of preemptive multi-threading is the recursive function let rec waitForStop () =promise (stop _ ↦→ promise (go _ ↦→ return ⟨ () ⟩ ) as p in (await p until ⟨ _ ⟩ in waitForStop ())) as p' in return p' which first installs an interrupt handler for stop , letting subsequent computations run their course.Once the stop interrupt arrives, the interrupt handler for it is triggered and the next one for go gets installed. In contrast to the interrupt handler for stop , the one for go starts awaiting the(unit) promise p . This means that any subsequent computations are blocked until a go interrupt isreceived, after which we recursively reinstall the interrupt handler for stop and repeat the cycle. Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:21 To initiate the preemptive behaviour for some computation comp , we simply run the program waitForStop () ; comp The algebraicity reduction rules for interrupt handlers ensure that they propagate out of waitForStop and encompass the entire computation, including comp . Observe that in contrast to usual effecthandler based encodings of multi-threading, waitForStop does not need any access to a thunk fun () ↦→ comp representing the threaded computation. In particular, the given computation comp can be completely unaware of the multi-threaded behaviour, both in its definition and its type.This approach can be easily extended to multiple threads, by using interrupts’ payloads tocommunicate thread IDs. To this end, we can consider interrupts stop : int and go : int , and define let rec waitForStop threadID =promise (stop threadID' when threadID = threadID' ↦→ promise (go threadID' when threadID = threadID' ↦→ return ⟨ () ⟩ ) as p inawait p until ⟨ _ ⟩ in waitForStop threadID) as p' in return p' using guarded interrupt handlers, and conditioning their triggering based on the received IDs. One of the main uses of asynchronous computation is to offload the execution of long-runningfunctions 𝑓 : 𝐴 → 𝐵 ! ( 𝑜, 𝜄 ) to remote processes. Below we show how to implement this in 𝜆 æ .One invokes a remote function by issuing a signal named call with the function’s argument ,and then awaits an interrupt named result with the function’s result , with all effects specified by ( 𝑜, 𝜄 ) happening at the callee site. The caller then calls such a remote function through a wrapper callWith , which issues the call signal, installs a handler for the result interrupt, and returns a thunkthat awaits the function’s result. For instance, one may then use remote functions in their code as let subtally = callWith "SELECT count(col) FROM table WHERE cond" inlet tally = callWith "SELECT count(col) FROM table" inprintf "Percentage: %d" (100 ∗ subtally () / tally ()) To avoid the results of earlier remote function calls from fulfilling the promises of later ones, weassign to each call a unique identifier, and communicate those in payloads. We implement theseunique identifiers using a counter. For a remote function 𝑓 : 𝐴 → 𝐵 ! ( 𝑜, 𝜄 ) , we type the signals andinterrupts as call : 𝐴 × int and result : 𝐵 × int . The caller site function callWith is then defined as let callWith x =let callNo = !callCounter in callCounter := !callCounter + 1 ; ↑ call (x, callNo) ; promise (result (y, callNo') when callNo = callNo' ↦→ return ⟨ y ⟩ ) as resultPromise inreturn (fun () → await resultPromise until ⟨ resultValue ⟩ in return resultValue) After issuing the call signal, callWith installs a guarded interrupt handler for the corresponding result interrupt, and then returns a function that, when called, awaits the result of the remote call.At the callee site , we simply install an interrupt handler that executes the function in question,issues an outgoing signal with the function’s result, and then recursively reinstalls itself, as follows: let remote f =let rec loop () =promise (call (x, callNo) ↦→ let y = f x in ↑ result (y, callNo) ; loop ()) as p in return pin loop () Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
Unlike effect handlers, our interrupt handlers have very limited control over the execution oftheir continuation. However, we can still simulate cancellations of asynchronous computations byawaiting a promise that will never be fulfilled. We achieve this with the help of the function let awaitCancel callNo runBeforeStall =promise (cancel callNo' when callNo = callNo' ↦→ promise (dummy () ↦→ return ⟨ () ⟩ ) as dummyPromise inrunBeforeStall () ; await dummyPromise until ⟨ _ ⟩ return ⟨ () ⟩ ) as _ in return () which takes the identifier of the remote function call that we want to make cancellable, and athunked computation to run before the continuation is stalled. We can then extend the callee sitewith cancellable function calls by invoking awaitCancel before we start executing the long-runningcomputation f x . In particular, we change the interrupt handler code in remote f to read as follows: call (x, callNo) ↦→ awaitCancel callNo loop ; let y = f x in ↑ result (y, callNo) ; loop () However, if left as is, cancelling one call would cancel all unfinished remote function calls becausethey would be part of the stalled continuation. To overcome this, we run the callee site in parallelwith an auxiliary process (which we omit here) that reacts to a cancel interrupt by reinvoking theseunfinished calls (minus the cancelled one) by reissuing the corresponding call signals, which thenget propagated to the callee site, and to the loop () we run in awaitCancel callNo loop before stalling.We note that the cancelled computation is only perpetually stalled , but not discarded completely,leading to a memory leak. We conjecture that extending 𝜆 æ with effect handlers that have greatercontrol over the continuation could lead to a more efficient code for the callee site. We also conjecturethat a future extension of 𝜆 æ with dynamic process creation would eliminate the need for theauxiliary reinvoker process, because then the callee site could create a new process for every remotefunction call it receives, and each cancel interrupt would stall only one of such (sub-)processes. Next, we use 𝜆 æ to implement a parallel variant of runners of algebraic effects [Ahman and Bauer2020]. These are a natural mathematical model and programming abstraction for resource man-agement based on algebraic effects, and correspond to effect handlers that apply continuations (atmost) once in a tail call position. In a nutshell, for a signature of operation symbols op : 𝐴 op → 𝐵 op ,a runner R comprises a family of stateful functions op R : 𝐴 op → 𝑅 ⇒ 𝐵 op × 𝑅 , called co-operations ,where 𝑅 is the type of resources that the runner manipulates. In the more general setting of Ahmanand Bauer [2020], the co-operations also model other, external effects, such as native calls to theoperating system, and can furthermore raise exceptions—all of which we shall gloss over here.Given a runner R , Ahman and Bauer [2020] provide the programmer with a construct using R @ 𝑉 init run 𝑀 finally { return 𝑥 @ 𝑟 fin ↦→ 𝑁 } which runs 𝑀 using R , with resources initially set to 𝑉 init ; and finalises the return value and finalresources using 𝑁 , e.g., ensuring that all file handles get closed. This is a form of effect handling: itexecutes 𝑀 by invoking co-operations in place of operation calls, while doing resource-passingunder the hood. Below we show by means of examples how one can use 𝜆 æ to naturally separate R and 𝑀 into different processes. For simplicity, we omit the initialisation and finalisation phases.For our first example, let us consider a runner that implements a pseudo-random number generator by providing a co-operation for random : → int , which we can for example implement as let linearCongruenceGeneratorRunner modulus a c initialSeed = Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:23 let rec loop seed =promise (randomReq callNo ↦→ let seed' = (a ∗ seed + c) mod modulus in ↑ randomRes (seed, callNo) ; loop seed') as p in return pin loop initialSeed It is given by a recursive interrupt handler, which listens for randomReq : int requests issued byclients, and itself issues randomRes : int × int responses. The resource this runner manages is theseed, which it passes between subsequent co-operation calls as an argument to the recursive loop .For the client code 𝑀 , we implement operation calls random () as discussed in Section 2.2, bydecoupling them into signals and interrupts. We again use guarded interrupt handlers and callidentifiers to avoid a response to one operation call fulfilling the promises of subsequent ones. let random () =let callNo = !callCounter in callCounter := callNo + 1 ; ↑ randomReq callNo ; promise (randomRes (n, callNo') when callNo = callNo' ↦→ return ⟨ n mod 10 ⟩ ) as p inawait p until ⟨ m ⟩ in return m As a second example, we show that this parallel approach to runners naturally extends to multipleco-operations. Specifically, we implement a runner for a heap , by providing co-operations for alloc : int → loc lookup : loc → int update : loc × int → We represent these co-operations using a signal/interrupt pair ( opReq , opRes ) with payload types type payloadReq = | AllocReq of int | LookupReq of loc | UpdateReq of loc ∗ inttype payloadRes = | AllocRes of loc | LookupRes of int | UpdateRes of unit The resulting runner is then implemented by pattern-matching on the payload value as follows: let rec heapRunner heap =promise (opReq (payloadReq, callNo) ↦→ let heap', payloadRes =match payloadReq with| AllocReq v ↦→ let heap', l = allocHeap heap v in return (heap', AllocRes l)| LookupReq l ↦→ let v = lookupHeap heap l in return (heap, LookupRes v)| UpdateReq (l, v) ↦→ let heap' = updateHeap heap l v in return (heap', UpdateRes ())in ↑ opRes (payloadRes, callNo) ; heapRunner heap') as p in return p Note that by storing heap in memory, we could have also used three signal/interrupt pairs and split heapRunner into three distinct interrupt handlers, one for each of allocation, lookup, and update.
As discussed in Section 2.4, interrupt handlers differ from ordinary operation calls by allowinguser-side post-processing of received data. In this final example, we show that 𝜆 æ is flexible enoughto modularly perform further non-blocking post-processing of this data anywhere in a program.For instance, let us assume we are writing a program that contains an interrupt handler (forsome op ) that promises to return us a list of integers. Now, at some later point in the program, wedecide that we want to further process this list if and when it becomes available, e.g., by using someof its elements to issue an outgoing signal. Of course, we could do this by going back and changing Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. the original interrupt handler, but this would not be very modular; nor do we want to block theentire program’s execution (using await ) until op arrives and the concrete list becomes available.Instead, we can define a generic combinator for non-blocking post-processing of promised values process op p with ( ⟨ x ⟩ ↦→ comp) as q in cont that takes an earlier made promise p (which we assume originates from handling the specifiedinterrupt op ), and makes a new promise to execute the post-processing code comp[v/x] once p getsfulfilled with some value v . The (non-blocking) continuation cont can refer to comp ’s result usingthe new promise-typed variable q bound in it. Under the hood, process op is a syntactic sugar for promise (op _ ↦→ await p until ⟨ x ⟩ in let y = comp in return ⟨ y ⟩ ) as q in cont While process op does involve an await , it gets exposed only after op is received, but by that time p will have been fulfilled with some v by an earlier interrupt handler, and thus the await can reduce.Returning to post-processing a list of integers promised by an existing interrupt handler, below isan example showing the use of the process op combinator and how to chain multiple post-processingcomputations together (here, filtering, folding, and issuing an outgoing signal), in the same spirit ashow one is taught to program compositionally with futures and promises [Haller et al. 2020]: promise (op x ↦→ original_interrupt_handler) as p in...process op p with ( ⟨ is ⟩ ↦→ filter (fun i ↦→ i > 0) is) as q inprocess op q with ( ⟨ js ⟩ ↦→ fold (fun j j' ↦→ j ∗ j') 1 js) as r inprocess op r with ( ⟨ k ⟩ ↦→ ↑ productOfPositiveElements k) as _ in... We note that for this to work, it is crucial that incoming interrupts behave like (deep) effect handling(see Section 3.2) so that all three post-processing computations get executed, in their program order.
We have shown how to incorporate asynchrony within algebraic effects, by decoupling the executionof operation calls into signalling that an operation’s implementation needs to be executed, andinterrupting a running computation with the operation’s result, to which it can react by installinginterrupt handlers. We have shown that our approach is flexible enough that not all signals haveto have a matching interrupt, and vice versa, allowing us to also model spontaneous behaviour,such as a user clicking a button or the environment preempting a thread. We have formalised theseideas in a small calculus, called 𝜆 æ , and demonstrated its flexibility on a number of examples. Wehave also accompanied the paper with an Agda formalisation and a prototype implementation of 𝜆 æ . However, various future work directions still remain. We discuss these and related work below. Asynchronous effects.
As asynchrony is desired in practice, it is no surprise that Koka [Leijen2017] and Multicore OCaml [Dolan et al. 2018], the two largest implementations of algebraiceffects and handlers, have been extended accordingly. In Koka, algebraic operations reify theircontinuation into an explicit callback structure that is then dispatched to a primitive such as setTimeout in its Node.JS backend. In Multicore OCaml, one uses low-level functions such as set_signal or timer_create that modify the runtime by interjecting operation calls inside the currentlyrunning code. Both approaches thus delegate the actual asynchrony to existing concepts in theirbackends. In contrast, in 𝜆 æ , we can express such backend features within the core calculus itself.Further, in 𝜆 æ , we avoid having to manually use (un)masking to disable asynchronous effects inunwanted places in our programs, which can be a very tricky business to get right, as noted byDolan et al. [2018]. Instead, by design, interrupts in 𝜆 æ never influence running code unless the Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:25 code has an explicit interrupt handler installed, and they always wait for any potential handler topresent itself during execution (recall that they get discarded only when reaching a return ). Message-passing.
While in this paper we have focussed on the foundations of asynchrony in thecontext of algebraic effects, the ideas we propose have also many common traits with concurrencymodels based on message-passing , such as the Actor model [Hewitt et al. 1973], the 𝜋 -calculus[Milner et al. 1992], and the join-calculus [Fournet and Gonthier 1996], just to name a few. Namely,one can view the issuing of a signal ↑ op ( 𝑉 , 𝑀 ) as sending a message, and handling an interrupt ↓ op ( 𝑊 , 𝑀 ) as receiving a message, along a channel named op . In fact, we believe that in ourprototype implementation we could replace the semantics presented in the paper with an equivalentone based on shared channels (one for each op ), to which the interrupt handlers could subscribe to.Instead of propagating signals first out and then in, they would be sent directly to channels whereinterrupt handlers immediately receive them, drastically reducing the cost of communication.Comparing 𝜆 æ to the Actor model, we see that the run 𝑀 processes evolve in their own bubbles,and only communicate with other processes via signals and interrupts, similarly to actors. However,in contrast to messages not being required to be ordered in the Actor model, in our 𝑃 || 𝑄 , the process 𝑄 receives interrupts in the same order as the respective signals are issued by 𝑃 (and vice versa).This communication ordering could be relaxed by allowing signals to be hoisted out of computationsfrom deeper than just the top level. Another difference with actors is that 𝜆 æ -computations can reactto interrupts only sequentially, and not by dynamically creating new parallel processes—first-classparallel processes and their dynamic creation is something we plan to address in future work.It is worth noting that our interrupt handlers are similar to the message receiving construct in the 𝜋 -calculus, in that they both synchronise with matching incoming interrupts/messages. However,the two are also different, in that interrupt handlers allow reductions to take place under them andnon-matching interrupts to propagate past them. Further, our interrupt handlers are also similarto join definitions in the join-calculus, describing how to react when a corresponding interruptarrives or join pattern appears, where in both cases the reaction could involve effectful code. Tothis end, our interrupt handlers resemble join definitions with simple one-channel join patterns.However, where the two constructs differ is that join definitions also serve to define new (local)channels, similarly to the restriction operator in the 𝜋 -calculus, whereas we assume a fixed globalset of channels (i.e., signal and interrupt names). We expect that extending 𝜆 æ with local algebraiceffects [Biernacki et al. 2019; Staton 2013] could help us fill this gap between the formalisms. Scoped operations.
As discussed in Section 3.2, despite their name, interrupt handlers behavelike algebraic operations, not like effect handlers. However, one should also note that they are notconventional operations because they carry computational data that sequential composition doesnot interact with, and that only gets triggered when a corresponding interrupt is received.Such generalised operations are known in the literature as scoped operations [Piróg et al. 2018], aleading example of which is spawn ( 𝑀 ; 𝑁 ) , where 𝑀 is the new child process to be executed and 𝑁 is the current process. Crucially, the child 𝑀 should not directly interact with the current process.Scoped operations achieve this behaviour by declaring 𝑀 to be in the scope of spawn , resulting in let 𝑥 = spawn ( 𝑀 ; 𝑁 ) in 𝐾 (cid:123) spawn ( 𝑀 ; let 𝑥 = 𝑁 in 𝐾 ) , exactly as we have for interrupt handlers.Further recalling Section 3.2, despite their appearance, incoming interrupts behave computation-ally like effect handling, not like algebraic operations. In fact, it turns out they correspond to effecthandling induced by an instance of scoped effect handlers [Piróg et al. 2018]. Compared to ordinaryeffect handlers, scoped effect handlers explain both how to interpret operations and their scopes.In our setting, this corresponds to selectively executing the handler code of interrupt handlers.It would be interesting to extend our work both with scoped operations having more generalsignatures, and with additional effect handlers for them. The latter could allow preventing the Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. propagation of incoming interrupts into continuations, discarding the continuation of a cancelledremote call, and techniques such as masking or reordering interrupts according to priority levels.
Modal types.
We recall that the type safety of 𝜆 æ crucially relies on the promise-typed variablesbound by interrupt handlers not being allowed to appear in the payloads of signals. This ensuresthat it is safe to propagate signals past all enveloping interrupt handlers, and communicate theirpayloads to other processes. In its essence, this is similar to the use of modal types in distributed[Murphy VII 2008] and reactive programming [Bahr et al. 2019; Krishnaswami 2013] to classifyvalues that can travel through space and time. In our case, it is the omission of promise types fromground types that allows us to consider the payloads of signals and interrupts as such mobile values .We expect that these connections to modal types will be key for extending 𝜆 æ with (i) higher-orderpayloads and (ii) process creation. For (i), we want to avoid the bodies of function-typed payloadsto be able to await enveloping promise variables to be fulfilled. For (ii), we want to do the same forthe dynamically created processes. In both cases, the reason is to be able to safely propagate thecorresponding programming constructs past enveloping interrupt handlers, and eventually hoistthem out of individual computations. We believe that the more structured treatment of contexts Γ ,as studied in various modal type systems, will hold the key for these extensions to be type safe. Denotational semantics.
In this paper we study only the operational side of 𝜆 æ , and leave devel-oping its denotational semantics for the future. In light of how we have motivated the 𝜆 æ -specificprogramming constructs, and based on the above discussions, we expect the denotational semanticsto take the form of an algebraically natural monadic semantics , where the monad would be givenby an instance of the one studied by Piróg et al. [2018] for scoped operations (quotiented by thecommutativity of signals and interrupt handlers, and extended with nondeterminism to modeldifferent evaluation outcomes), incoming interrupts would be modelled as homomorphisms inducedby scoped algebras, and parallel composition by considering all nondeterministic interleavings of(the outgoing signals of) individual computations, e.g., based on how Plotkin [2012] and Lindleyet al. [2017] model it in the context of general effect handlers. Finally, we expect to take inspirationfor the denotational semantics of the promise type from that of modal logics and modal types. Reasoning about asynchronous effects.
In addition to using 𝜆 æ ’s type-and-effect system only forspecification purposes (such as specifying that 𝑀 : 𝑋 ! (∅ , {}) raises no signals and installs nointerrupt handlers), we wish to make further use of it for validating effect-dependent optimisations [Kammar and Plotkin 2012]. For instance, whenever 𝑀 : 𝑋 ! ( 𝑜, 𝜄 ) and 𝜄 ( op ) = ⊥ , we would like toknow that ↓ op ( 𝑉 , 𝑀 ) (cid:123) ∗ 𝑀 . One way to validate such optimisations is to develop an adequatedenotational semantics, and then use a semantic computational induction principle [Bauer andPretnar 2014; Plotkin and Pretnar 2008]. For 𝜆 æ , this would amount to only having to prove theoptimisations for return values, signals, and interrupt handlers. Another way to validate effect-dependent optimisations would be to define a suitable logical relation for 𝜆 æ [Benton et al. 2014].In addition to optimisations based on 𝜆 æ ’s existing effect system, we plan to explore extendingprocesses and their types with communication protocols inspired by session types [Honda et al.1998], so as to refine the current “broadcast everything everywhere” communication strategy. ACKNOWLEDGEMENTS
We thank the anonymous reviewers, Otterlo IFIP WG 2.1 meeting participants, and Andrej Bauer,Gavin Bierman, Žiga Lukšič, and Alex Simpson for their useful feedback. This project has receivedfunding from the European Union’s Horizon 2020 research and innovation programme underthe Marie Skłodowska-Curie grant agreement No 834146 . This material is based upon worksupported by the Air Force Office of Scientific Research under award number FA9550-17-1-0326.
Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021. synchronous Effects 24:27
REFERENCES
D. Ahman. 2020. Agda formalisation of the 𝜆 æ -calculus. Available at https://github.com/danelahman/aeff-agda/releases/tag/popl-2021.D. Ahman and A. Bauer. 2020. Runners in action. In Proc. of 29th European Symp. on Programming, ESOP 2020 (LNCS,Vol. 12075) . Springer, 29–55.D. Ahman and M. Pretnar. 2020. Software artefact for the POPL 2021 paper "Asynchronous Effects". Available at https://doi.org/10.5281/zenodo.4072753.R. M. Amadio and P-L. Curien. 1998.
Domains and Lambda Calculi . Cambridge University Press.P. Bahr, C. Graulund, and R. E. Mogelberg. 2019. Simply RaTT: a fitch-style modal calculus for reactive programmingwithout space leaks.
Proc. ACM Program. Lang.
3, ICFP (2019), 109:1–109:27.A. Bauer and M. Pretnar. 2014. An Effect System for Algebraic Effects and Handlers.
Logical Methods in Computer Science
10, 4 (2014).A. Bauer and M. Pretnar. 2015. Programming with algebraic effects and handlers.
J. Log. Algebr. Meth. Program.
84, 1 (2015),108–123.N. Benton, M. Hofmann, and V. Nigam. 2014. Abstract effects and proof-relevant logical relations. In
Proc. of 41st Ann. ACMSIGPLAN-SIGACT Symp. on Principles of Programming Languages, POPL 2014 . ACM, 619–632.D. Biernacki, M. Piróg, P. Polesiuk, and F. Sieczkowski. 2019. Abstracting Algebraic Effects.
Proc. ACM Program. Lang.
J. Mach. Learn. Res.
20, 1 (Jan. 2019), 973–978.L. Convent, S. Lindley, C. McBride, and C. McLaughlin. 2020. Doo bee doo bee doo.
J. Funct. Program.
30 (2020), e9.S. Dolan, S. Eliopoulos, D. Hillerström, A. Madhavapeddy, K. C. Sivaramakrishnan, and L. White. 2018. Concurrent SystemProgramming with Effect Handlers. In
Proc. of 18th Int. Sym. Trends in Functional Programming, TFP 2017 . Springer,98–117.C. Fournet and G. Gonthier. 1996. The Reflexive CHAM and the Join-Calculus. In
Proc. of 23rd ACM SIGPLAN-SIGACT Symp.on Principles of Programming Languages, POPL’96 . ACM, 372–385.G. Gierz, K. H. Hofmann, K. Keimel, J. D. Lawson, M. Mislove, and D. S. Scott. 2003.
Continuous Lattices and Domains .Number 93 in Encyclopedia of Mathematics and its Applications. Cambridge University Press.P. Haller, A. Prokopec, H. Miller, V. Klang, R. Kuhn, and V. Jovanovic. 2020. Scala documentation: Futures and Promises.(July 2020). Available online at https://docs.scala-lang.org/overviews/core/futures.html.C. Hewitt, P. Bishop, and R. Steiger. 1973. A Universal Modular ACTOR Formalism for Artificial Intelligence. In
Proc. of 3rdInt. Joint Conf. on Artificial Intelligence, IJCAI’73 . Morgan Kaufmann Publishers Inc., 235–245.K. Honda, V. T. Vasconcelos, and M. Kubo. 1998. Language Primitives and Type Discipline for Structured Communication-Based Programming. In
Proc. of 7th European Symp. on Programming, ESOP 1998 (LNCS, Vol. 1381) . Springer, 122–138.O. Kammar, S. Lindley, and N. Oury. 2013. Handlers in Action. In
Proc. of 18th ACM SIGPLAN Int. Conf. on FunctionalProgramming, ICFP 2013 . ACM, 145–158.O. Kammar and G. D. Plotkin. 2012. Algebraic foundations for effect-dependent optimisations. In
Proc. of 39th ACMSIGPLAN-SIGACT Symp. on Principles of Programming Languages, POPL 2012 . ACM, 349–360.N. R. Krishnaswami. 2013. Higher-Order Functional Reactive Programming without Spacetime Leaks. In
Proc of 18th ACMSIGPLAN Int. Conf. on Functional Programming, ICFP 2013 . ACM, 221–232.D. Leijen. 2017. Structured asynchrony with algebraic effects. In
Proc. of 2nd ACM SIGPLAN Int. Wksh. on Type-DrivenDevelopment, TyDe@ICFP 2017 . ACM, 16–29.P. B. Levy, J. Power, and H. Thielecke. 2003. Modelling environments in call-by-value programming languages.
Inf. Comput.
Proc. of 44th ACM SIGPLAN Symp. on Principles ofProgramming Languages, POPL 2017 . ACM, 500–514.R. Milner, J. Parrow, and D. Walker. 1992. A calculus of mobile processes, I.
Inf. Comput.
Modal Types for Mobile Code . Ph.D. Dissertation. Carnegie Mellon University.M. Piróg, T. Schrijvers, N. Wu, and M. Jaskelioff. 2018. Syntax and Semantics for Operations with Scopes. In
Proc. of 33rdAnnual ACM/IEEE Symp. on Logic in Computer Science, LICS 2018 . ACM, 809–818.G. D. Plotkin. 2012. Concurrency and the algebraic theory of effects. (2012). Invited talk at the 23rd Int. Conf. on ConcurrencyTheory, CONCUR 2012.G. D. Plotkin and J. Power. 2002. Notions of Computation Determine Monads. In
Proc. of 5th Int. Conf. on Foundations ofSoftware Science and Computation Structures, FOSSACS 2002 (LNCS, Vol. 2303) . Springer, 342–356.G. D. Plotkin and M. Pretnar. 2008. A Logic for Algebraic Effects. In
Proc. of 23th Ann. IEEE Symp. on Logic in ComputerScience, LICS 2008 . IEEE, 118–129.G. D. Plotkin and M. Pretnar. 2013. Handling Algebraic Effects.
Logical Methods in Computer Science
9, 4:23 (2013).Proc. ACM Program. Lang., Vol. 5, No. POPL, Article 24. Publication date: January 2021.
L. Poulson. 2020.
Asynchronous Effect Handling . Master’s thesis. School of Informatics, University of Edinburgh.M. Pretnar. 2015. An Introduction to Algebraic Effects and Handlers. Invited tutorial paper.
Electr. Notes Theor. Comput. Sci.
319 (2015), 19–35.M. Pretnar. 2020. Programming language Æff. Available at https://github.com/matijapretnar/aeff/releases/tag/popl-2021.J. Schwinghammer. 2002.
A Concurrent Lambda-Calculus with Promises and Futures . Master’s thesis. Programming SystemsLab, Universität des Saarlandes.S. Staton. 2013. Instances of Computational Effects: An Algebraic Perspective. In
Proc. of 28th Ann. ACM/IEEE Symp. onLogic in Computer Science, LICS 2013 . IEEE, 519–519.S. Staton. 2015. Algebraic Effects, Linearity, and Quantum Programming Languages. In
Proc. of 42nd Annual ACM SIGPLAN-SIGACT Symp. on Principles of Programming Languages, POPL 2015 . ACM, 395–406.A. K. Wright and M. Felleisen. 1994. A Syntactic Approach to Type Soundness.