Runners in Action

Runners of algebraic effects, also known as comodels, provide a mathematical model of resource management. We show that they also give rise to a programming concept that models top-level external resources, as well as allows programmers to modularly define their own intermediate “virtual machines”. We capture the core ideas of programming with runners in an equational calculus \documentclass[12pt]{minimal} \usepackage{amsmath} \usepackage{wasysym} \usepackage{amsfonts} \usepackage{amssymb} \usepackage{amsbsy} \usepackage{mathrsfs} \usepackage{upgreek} \setlength{\oddsidemargin}{-69pt} \begin{document}$$\lambda _{\mathsf {coop}}$$\end{document}λcoop, which we equip with a sound and coherent denotational semantics that guarantees the linear use of resources and execution of finalisation code. We accompany \documentclass[12pt]{minimal} \usepackage{amsmath} \usepackage{wasysym} \usepackage{amsfonts} \usepackage{amssymb} \usepackage{amsbsy} \usepackage{mathrsfs} \usepackage{upgreek} \setlength{\oddsidemargin}{-69pt} \begin{document}$$\lambda _{\mathsf {coop}}$$\end{document}λcoop with examples of runners in action, provide a prototype language implementation in OCaml, as well as a Haskell library based on \documentclass[12pt]{minimal} \usepackage{amsmath} \usepackage{wasysym} \usepackage{amsfonts} \usepackage{amssymb} \usepackage{amsbsy} \usepackage{mathrsfs} \usepackage{upgreek} \setlength{\oddsidemargin}{-69pt} \begin{document}$$\lambda _{\mathsf {coop}}$$\end{document}λcoop.


Introduction
Computational effects, such as exceptions, input-output, state, nondeterminism, and randomness, are an important component of general-purpose programming languages, whether they adopt functional, imperative, object-oriented, or other programming paradigms. Even pure languages exhibit computational effects at the top level, so to speak, by interacting with their external environment.
In modern languages, computational effects are often structured using monads [22,23,36], or algebraic effects and handlers [12,28,30]. These mechanisms excel at implementation of computational effects within the language itself. For instance, the familiar implementation of mutable state in terms of state-passing functions requires no native state, and can be implemented either as a monad or using handlers. One is naturally drawn to using these techniques also for dealing with actual effects, such as manipulation of native memory and access to hardware. These are represented inside the language as algebraic operations (as in Eff [4]) or a monad (in the style of Haskell's IO), but treated specially by the language's top-level runtime, which invokes corresponding operating system functionality. While this approach works in practice, it has some unfortunate downsides too, namely lack of modularity and linearity, and excessive generality.
Lack of modularity is caused by having the external resources hard-coded into the top-level runtime. As a result, changing which resources are available and how they are implemented requires modifications of the language implementation. Additional complications arise when a language supports several operating systems and hardware platforms, each providing their own, different feature set.
One wishes that the ingenuity of the language implementors were better supported by a more flexible methodology with a sound theoretical footing.
Excessive generality is not as easily discerned, because generality of programming concepts makes a language expressive and useful, such as general algebraic effects and handlers enabling one to implement timeouts, rollbacks, stream redirection [30], async & await [16], and concurrency [9]. However, the flip side of such expressive freedom is the lack of any guarantees about how external resources will actually be used. For instance, consider a simple piece of code, written in Eff-like syntax, which first opens a file, then writes to it, and finally closes it: let fh = open "hello.txt" in write (fh, "Hello, world."); close fh What this program actually does depends on how the operations open, write, and close are handled. For all we know, an enveloping handler may intercept the write operation and discard its continuation, so that close never happens and the file is not properly closed. Telling the programmer not to shoot themselves in the foot by avoiding such handlers is not helpful, because the handler may encounter an external reason for not being able to continue, say a full disk.
Even worse, external resources may be misused accidentally when we combine two handlers, each of which works as intended on its own. For example, if we combine the above code with a non-deterministic choose operation, as in The resulting program attempts to close the file twice, as well as write to it twice, because the continuation k is invoked twice when handling choose. Of course, with enough care all such situations can be dealt with, but that is beside the point. It is worth sacrificing some amount of the generality of algebraic effects and monads in exchange for predictable and safe usage of external computational effects, so long as the vast majority of common use cases are accommodated.
Contributions We address the described issues by showing how to design a programming language based on runners of algebraic effects. We review runners in §2 and recast them as a programming construct in §3. In §4, we present λ coop , a calculus that captures the core ideas of programming with runners. We provide a coherent and sound denotational semantics for λ coop in §5, where we also prove that well-typed code is properly finalised. In §6, we show examples of runners in action. The paper is accompanied by a prototype language Coop and a Haskell library Haskell-Coop, based on λ coop , see §7. The relationship between λ coop and existing work is addressed in §8, and future possibilities discussed in §9.
The paper is also accompanied by an online appendix (https://arxiv.org/ abs/1910.11629) that provides the typing and equational rules we omit in §4.
Runners are modular in that they can be used not only to model the toplevel interaction with the external environment, but programmers can also use them to define and nest their own intermediate "virtual machines". Our runners are effectful : they may handle operations by calling further outer operations, and raise exceptions and send signals, through which exceptional conditions and runtime errors are communicated back to user programs in a safe fashion that preserves linear usage of external resources and ensures their proper finalisation.
We achieve suitable generality for handling of external resources by showing how runners provide implementations of algebraic operations together with a natural notion of finalisation, and a strong guarantee that in the absence of external kill signals the finalisation code is executed exactly once (Thm. 7). We argue that for most purposes such discipline is well worth having, and giving up the arbitrariness of effect handlers is an acceptable price to pay. In fact, as will be apparent in the denotational semantics, runners are simply a restricted form of handlers, which apply the continuation at most once in a tail call position.
Runners guarantee linear usage of resources not through a linear or uniqueness type system (such as in the Clean programming language [15]) or a syntactic discipline governing the application of continuations in handlers, but rather by a design based on the linear state-passing technique studied by Møgelberg and Staton [21]. In this approach, a computational resource may be implemented without restrictions, but is then guaranteed to be used linearly by user code.

Algebraic effects, handlers, and runners
We begin with a short overview of the theory of algebraic effects and handlers, as well as runners. To keep focus on how runners give rise to a programming concept, we work naively in set theory. Nevertheless, we use category-theoretic language as appropriate, to make it clear that there are no essential obstacles to extending our work to other settings (we return to this point in §5.1).

Algebraic effects and handlers
There is by now no lack of material on the algebraic approach to structuring computational effects. For an introductory treatment we refer to [5], while of course also recommend the seminal papers by Plotkin and Power [25,28]. The brief summary given here only recalls the essentials and introduces notation.
An (algebraic) signature is given by a set Σ of operation symbols, and for each op P Σ its operation signature op : A op B op , where A op and B op are called the parameter and arity set. A Σ-structure M is given by a carrier set |M|, and for each operation symbol op P Σ, a map op M : A opˆp B op ñ |M|q Ñ |M|, where ñ is set exponentiation. The free Σ-structure Tree Σ pXq over a set X is the set of well-founded trees generated inductively by return x P Tree Σ pXq, for every x P X, and oppa, κq P Tree Σ pXq, for every op P Σ, a P A op , and κ : B op Ñ Tree Σ pXq.
We are abusing notation in a slight but standard way, by using op both as the name of an operation and a tree-forming constructor. The elements of Tree Σ pXq are called computation trees: a leaf return x represents a pure computation returning a value x, while oppa, κq represents an effectful computation that calls op with parameter a and continuation κ, which expects a result from B op .
An algebraic theory T " pΣ T , Eq T q is given by a signature Σ T and a set of equations Eq T . The equations Eq T express computational behaviour via interactions between operations, and are written in a suitable formalism, e.g., [30]. We explain these by way of examples, as the precise details do not matter for our purposes. Let 0 " t u be the empty set and 1 " t‹u the standard singleton.
For example, the second equation states that reading state right after setting it to c gives precisely c. The third equation states that setenv overwrites the state.
Example 2. Given a set of exceptions E, the algebraic theory of E-many exceptions is given by a single operation raise : E 0, and no equations.
A T -model, also called a T -algebra, is a Σ T -structure which satisfies the equations in Eq T . The free T -model over a set X is constructed as the quotient by the Σ T -congruence " generated by Eq T . Each op P Σ T is interpreted in the free model as the map pa, κq Þ Ñ roppa, κqs, where r´s is the "-equivalence class.
Free T p´q is the functor part of a monad on sets, whose unit at a set X is The Kleisli extension for this monad is then the operation which lifts any map f : X Ñ Tree Σ T pY q to the map f : : Free Σ T pXq Ñ Free Σ T pY q, given by That is, f : traverses a computation tree and replaces each leaf return x with f x.
The preceding construction of free models and the monad may be retrofitted to an algebraic signature Σ, if we construe Σ as an algebraic theory with no equations. In this case " is just equality, and so we may omit the quotient and the pesky equivalence classes. Thus the carrier of the free Σ-model is the set of well-founded trees Tree Σ pXq, with the evident monad structure.
A fundamental insight of Plotkin and Power [25,28] was that many computational effects may be adequately described by algebraic theories, with the elements of free models corresponding to effectful computations. For example, the monads induced by the theories from Examples 1 and 2 are respectively isomorphic to the usual state monad St C X def " pC ñ XˆCq and the exceptions monad Exc E X def " X`E. Plotkin and Pretnar [30] further observed that the universal property of free models may be used to model a programming concept known as handlers. Given a T -model M and a map f : X Ñ |M|, the universal property of the free T -model gives us a unique T -homomorphism f ; : Free T pXq Ñ |M| satisfying A handler for a theory T in a language such as Eff amounts to a model M whose carrier |M| is the carrier Free T 1 pY q of the free model for some other theory T 1 , while the associated handling construct is the induced T -homomorphism Free T pXq Ñ Free T 1 pY q. Thus handling transforms computations with effects T to computations with effects T 1 . There is however no restriction on how a handler implements an operation, in particular, it may use its continuation in an arbitrary fashion. We shall put the universal property of free models to good use as well, while making sure that the continuations are always used affinely.

Runners
Much like monads, handlers are useful for simulating computational effects, because they allow us to transform T -computations to T 1 -computations. However, eventually there has to be a "top level" where such transformations cease and actual computational effects happen. For these we need another concept, known as runners [35]. Runners are equivalent to the concept of comodels [27,31], which are "just models in the opposite category", although one has to apply the motto correctly by using powers and co-powers where seemingly exponentials and products would do. Without getting into the intricacies, let us spell out the definition. Definition 1. A runner R for a signature Σ is given by a carrier set |R| together with, for each op P Σ, a co-operation op R : A op Ñ p|R| ñ B opˆ| R|q.
Runners are usually defined to have co-operations in the equivalent uncurried form op R : A opˆ| R| Ñ B opˆ| R|, but that is less convenient for our purposes.
Runners may be defined more generally for theories T , rather than just signatures, by requiring that the co-operations satisfy Eq T . We shall have no use for these, although we expect no obstacles in incorporating them into our work.
A runner tells us what to do when an effectful computation reaches the top-level runtime environment. Think of |R| as the set of configurations of the runtime environment. Given the current configuration c P |R|, the operation oppa, κq is executed as the corresponding co-operation op R a c whose result pb, c 1 q P B opˆ| R| gives the result of the operation b and the next runtime configuration c 1 . The continuation κ b then proceeds in runtime configuration c 1 .
It is not too difficult to turn this idea into a mathematical model. For any X, the co-operations induce a Σ-structure M with |M| def " St |R| X " p|R| ñ Xˆ|R|q and operations op M : We may then use the universal property of the free Σ-model to obtain a Σhomomorphism r X : Tree Σ pXq Ñ St |R| X satisfying the equations r X preturn xq " λc . px, cq, r X poppa, κqq " op M pa, r X˝κ q.
The map r X precisely captures the idea that a runner runs computations by transforming (static) computation trees into state-passing maps. Note how in the above definition of op M , the continuation κ is used in a controlled way, as it appears precisely once as the head of the outermost application. In terms of programming, this corresponds to linear use in a tail-call position.
Runners are less ad-hoc than they may seem. First, notice that op M is just the composition of the co-operation op R with the state monad's Kleisli extension of the continuation κ, and so is the standard way of turning generic effects into Σstructures [26]. Second, the map r X is the component at X of a monad morphism r : Tree Σ p´q Ñ St |R| . Møgelberg & Staton [21], as well as Uustalu [35], showed that the passage from a runner R to the corresponding monad morphism r forms a one-to-one correspondence between the former and the latter.
As defined, runners are too restrictive a model of top-level computation, because the only effect available to co-operations is state, but in practice the runtime environment may also signal errors and perform other effects, by calling its own runtime environment. We are led to the following generalisation.
Definition 2. For a signature Σ and monad T , a T -runner R for Σ, or just an effectful runner, is given by, for each op P Σ, a co-operation op R : The correspondence between runners and monad morphisms still holds.
Proposition 3. For a signature Σ and a monad T , the monad morphisms Tree Σ p´q Ñ T are in one-to-one correspondence with T -runners for Σ.
Proof. This is an easy generalisation of the correspondence for ordinary runners. Let us fix a signature Σ, and a monad T with unit η and Kleisli extension´:.
Let R be a T -runner for Σ. For any set X, R induces a Σ-structure M with |M| def " T X and op M : A opˆp B op ñ T Xq Ñ T X defined as op M pa, κq def " κ : pop R aq. As before, the universal property of the free model Tree Σ pXq provides a unique Σ-homomorphism r X : Tree Σ pXq Ñ T X, satisfying the equations r X preturn xq " η X pxq, r X poppa, κqq " op M pa, r X˝κ q.
The maps r X collectively give us the desired monad morphism r induced by R.
Conversely, given a monad morphism θ : Tree Σ p´q Ñ T , we may recover a Trunner R for Σ by defining the co-operations as op R a def " θ Bop poppa, λb . return bqq. It is not hard to check that we have described a one-to-one correspondence.

Programming with runners
If ordinary runners are not general enough, the effectful ones are too general: parameterised by arbitrary monads T , they do not combine easily and they lack a clear notion of resource management. Thus, we now engineer more specific monads whose associated runners can be turned into a programming concept. While we give up complete generality, the monads presented below are still quite versatile, as they are parameterised by arbitrary algebraic signatures Σ, and so are extensible and support various combinations of effects.

The user and kernel monads
Effectful source code running inside a runtime environment is just one example of a more general phenomenon in which effectful computations are enveloped by a layer that provides a supervised access to external resources: a user process is controlled by a kernel, a web page by a browser, an operating system by hardware, or a virtual machine, etc. We shall adopt the parlance of software systems, and refer to the two layers generically as the user and kernel code.
Since the two kinds of code need not, and will not, use the same effects, each will be described by its own algebraic theory and compute in its own monad. We first address the kernel theory. Specifically, we look for an algebraic theory such that effectful runners for the induced monad satisfy the following desiderata: 1. Runners support management and controlled finalisation of resources. 2. Runners may use further external resources. 3. Runners may signal failure caused by unavoidable circumstances.
The totality of external resources available to user code appears as a stateful external environment, even though it has no direct access to it. Thus, kernel computations should carry state. We achieve this by incorporating into the kernel theory the operations getenv and setenv, and equations for state from Example 1.
Apart from managing state, kernel code should have access to further effects, which may be true external effects, or some outer layer of runners. In either case, we should allow the kernel code to call operations from a given signature Σ.
Because kernel computations ought to be able to signal failure, we should include an exception mechanism. In practice, many programming languages and systems have two flavours of exceptions, variously called recoverable and fatal, checked and unchecked, exceptions and errors, etc. One kind, which we call just exceptions, is raised by kernel code when a situation requires special attention by user code. The other kind, which we call signals, indicates an unrecoverable condition that prevents normal execution of user code. These correspond precisely to the two standard ways of combining exceptions with state, namely the coproduct and the tensor of algebraic theories [11]. The coproduct simply adjoins exceptions raise : E 0 from Example 2 to the theory of state, while the tensor extends the theory of state with signals kill : S 0, together with equations getenvpλc . kill sq " kill s, setenvpc, kill sq " kill s.
These equations say that a signal discards state, which makes it unrecoverable. To summarise, the kernel theory K Σ,E,S,C contains operations from a signature Σ, as well as state operations getenv : 1 C, setenv : C 1, exceptions raise : E 0, and signals kill : S 0, with equations for state from Example 1, equations (1) relating state and signals, and for each operation op P Σ, equations getenvpλc . oppa, κ cqq " oppa, λb . getenvpλc . κ c bqq, setenvpc, oppa, κqq " oppa, λb . setenvpc, κ bqq, expressing that external operations do not interact with kernel state. It is not difficult to see that K Σ,E,S,C induces, up to isomorphism, the kernel monad How about user code? It can of course call operations from a signature Σ (not necessarily the same as the kernel code), and because we intend it to handle exceptions, it might as well have the ability to raise them. However, user code knows nothing about signals and kernel state. Thus, we choose the user theory U Σ,E to be the algebraic theory with operations Σ, exceptions raise : E 0, and no equations. This theory induces the user monad U Σ,E X def " Tree Σ pX`Eq.

Runners as a programming construct
In this section, we turn the ideas presented so far into programming constructs. We strive for a realistic result, but when faced with several design options, we prefer simplicity and semantic clarity. We focus here on translating the central concepts, and postpone various details to §4, where we present a full calculus.
We codify the idea of user and kernel computations by having syntactic categories for each of them, as well as one for values. We use letters M , N to indicate user computations, K, L for kernel computations, and V , W for values.
User and kernel code raise exceptions with operation raise, and catch them with exception handlers based on Benton and Kennedy's exceptional syntax [7], try M with treturn x Þ Ñ N, . . . , raise e Þ Ñ N e , . . .u, and analogously for kernel code. The familiar binding construct let x " M in N is simply shorthand for try M with treturn x Þ Ñ N, . . . , raise e Þ Ñ raise e, . . .u.
As a programming concept, a runner R takes the form where each K op is a kernel computation, with the variable x bound in K op , so that each clause op x Þ Ñ K op determines a co-operation for the kernel monad. The subscript C indicates the type of the state used by the kernel code K op . The corresponding elimination form is a handling-like construct which uses the co-operations of runner R "at" initial kernel state V to run user code M , and finalises its return value, exceptions, and signals with F , see (3) below. When user code M calls an operation op, the enveloping run construct runs the corresponding co-operation K op of R. While doing so, K op might raise exceptions. But not every exception makes sense for every operation, and so we assign to each operation op a set of exceptions E op which the co-operations implementing it may raise, by augmenting its operation signature with E op , as An exception raised by the co-operation K op propagates back to the operation call in the user code. Therefore, an operation call should have not only a continuation x . M receiving a result, but also continuations N e , one for each e P E op , oppV, px . M q, pN e q ePEop q.
If K op returns a value b P B op , the execution proceeds as M rb{xs, and as N e if K op raises an exception e P E op . In examples, we use the generic versions of operations [26], written op V , which pass on return values and re-raise exceptions.
One can pass exceptions back to operation calls also in a language with handlers, such as Eff, by changing the signatures of operations to A op B op`Eop , and implementing the exception mechanism by hand, so that every operation call is followed by a case distinction on B op`Eop . One is reminded of how operating system calls communicate errors back to user code as exceptional values.
A co-operation K op may also send a signal, in which case the rest of the user code M is skipped and the control proceeds directly to the corresponding case of the finalisation part F of the run construct (2), whose syntactic form is Specifically, if M returns a value v, then N is evaluated with x bound to v and c to the final kernel state; if M raises an exception e (either directly or indirectly via a co-operation of R), then N e is executed, again with c bound to the final kernel state; and if a co-operation of R sends a signal s, then N s is executed.
Example 4. In anticipation of setting up the complete calculus we show how one can work with files. The language implementors can provide an operation open which opens a file for writing and returns its file handle, an operation close which closes a file handle, and a runner fileIO that implements writing. Let us further suppose that fileIO may raise an exception QuotaExceeded if a write exceeds the user disk quota, and send a signal IOError if an unrecoverable external error occurs. The following code illustrates how to guarantee proper closing of the file: Notice that the user code does not have direct access to the file handle. Instead, the runner holds it in its state, where it is available to the co-operation that implements write. The finalisation block gets access to the file handle upon successful completion and raised exception, so it can close the file, but when a signal happens the finalisation cannot close the file, nor should it attempt to do so. We also mention that the code "cheats" by placing the call to open in a position where a value is expected. We should have let-bound the file handle returned by open outside the run construct, which would make it clear that opening the file happens before this construct (and that open is not handled by the finalisation), but would also expose the file handle. Since there are clear advantages to keeping the file handle inaccessible, a realistic language should accept the above code and hoist computations from value positions automatically.

A calculus for programming with runners
Inspired by the semantic notion of runners and the ideas of the previous section, we now present a calculus for programming with co-operations and runners, called λ coop . It is a low-level fine-grain call-by-value calculus [19], and as such could inspire an intermediate language that a high-level language is compiled to.

Types
The types of λ coop are shown in Fig. 1. The ground types contain base types, and are closed under finite sums and products. These are used in operation signatures and as types of kernel state. (Allowing arbitrary types in either of these entails substantial complications that can be dealt with but are tangential to our goals.) Ground types can also come with corresponding constant symbols f, each associated with a fixed constant signature f : pA 1 , . . . , A n q Ñ B.
We assume a supply of operation symbols O, exception names E, and signal names S. Each operation symbol op P O is equipped with an operation signature A op B op ! E op , which specifies its parameter type A op and arity type B op , and the exceptions E op that the corresponding co-operations may raise in runners.
The value types extend ground types with two function types, and a type of runners. The user function type X Ñ Y ! pΣ, Eq classifies functions taking arguments of type X to computations classified by the user (computation) type Y ! pΣ, Eq, i.e., those that return values of type Y , and may call operations Σ and raise exceptions E. Similarly, the kernel function type X Ñ Y pΣ, E, S, Cq classifies functions taking arguments of type X to computations classified by the kernel (computation) type Y pΣ, E, S, Cq, i.e., those that return values of type Y , and may call operations Σ, raise exceptions E, send signals S, and use state of type C. We note that the ingredients for user and kernel types correspond precisely to the parameters of the user monad U Σ,E and the kernel monad K Σ,E,S,C from §3.1. Finally, the runner type Σ ñ pΣ 1 , S, Cq classifies runners that implement co-operations for the operations Σ as kernel computations which use operations Σ 1 , send signals S, and use state of type C.

Values and computations
The syntax of terms is shown in Fig. 2. The usual fine-grain call-by-value stratification of terms into pure values and effectful computations is present, except that we further distinguish between user and kernel computations.
Values Among the values are variables, constants for ground types, and constructors for sums and products. There are two kinds of functions, for abstracting over user and kernel computations. A runner is a value of the form It implements co-operations for operations op as kernel computations K op , with x bound in K op . The type annotation C specifies the type of the state that K op uses. Note that C ranges over ground types, a restriction that allows us to define a naive set-theoretic semantics. We sometimes omit these type annotations.
User and kernel computations The user and kernel computations both have pure computations, function application, exception raising and handling, stan- dard elimination forms, and operation calls. Note that the typing annotations on some of these differ according to their mode. For instance, a user operation call is annotated with the result type X, whereas the annotation X @ C on a kernel operation call also specifies the kernel state type C. The binding construct let X!E x " M in N is not part of the syntax, but is an abbreviation for try M with treturn x Þ Ñ N, praise e Þ Ñ raise X eq ePE u, and there is an analogous one for kernel computations. We often drop the annotation X!E.
Some computations are specific to one or the other mode. Only the kernel mode may send a signal with kill, and manipulate state with getenv and setenv, but only the user mode has the run construct from §3.2. Finally, each mode has the ability to "context switch" to the other one. The kernel computation user M with treturn x Þ Ñ K, praise e Þ Ñ L e q ePE u runs a user computation M and handles the returned value and leftover exceptions with kernel computations K and L e . Conversely, the user computation kernel K @ W finally tx @ c Þ Ñ M, praise e @ c Þ Ñ N e q ePE , pkill s Þ Ñ N s q sPS u runs kernel computation K with initial state W , and handles the returned value, and leftover exceptions and signals with user computations M , N e , and N s .

Type system
We equip λ coop with a type system akin to type and effect systems for algebraic effects and handlers [3,7,12]. We are experimenting with resource control, so it makes sense for the type system to tightly control resources. Consequently, our effect system does not allow effects to be implicitly propagated outwards.
In §4.1, we assumed that each operation op P O is equipped with some fixed operation signature op : A op B op ! E op . We also assumed a fixed constant signature f : pA 1 , . . . , A n q Ñ B for each ground constant f. We consider this information to be part of the type system and say no more about it.
Values, user computations, and kernel computations each have a corresponding typing judgement form and a subtyping relation, given by where Γ is a typing context x 1 : X 1 , . . . , x n : X n . The effect information is an over-approximation, i.e., M and K employ at most the effects described by U and K. The complete rules for these judgements are given in the online appendix.
We comment here only on the rules that are peculiar to λ coop , see Fig. 3. Subtyping of ground types Sub-Ground is trivial, as it relates only equal types. Subtyping of runners Sub-Runner and kernel computations Sub-Kernel requires equality of the kernel state types C and C 1 because state is used invariantly in the kernel monad. We leave it for future work to replace C " C 1 with a lens [10] from C 1 to C, i.e., maps C 1 Ñ C and C 1ˆC Ñ C 1 satisfying state equations analogous to Example 1. It has been observed [24,31] that such a lens in fact amounts to an ordinary runner for C-valued state.

Sub-Ground
The rules TyUser-Op and TyKernel-Op govern operation calls, where we have a success continuation which receives a value returned by a co-operation, and exceptional continuations which receive exceptions raised by co-operations.
The rule TyUser-Run requires that the runner V implements all the operations M can use, meaning that operations are not implicitly propagated outside a run block (which is different from how handlers are sometimes implemented). Of course, the co-operations of the runner may call further external operations, as recorded by the signature Σ 1 . Similarly, we require the finally block F to intercept all exceptions and signals that might be produced by the co-operations of V or the user code M . Such strict control is exercised throughout. For example, in TyUser-Run, TyUser-Kernel, and TyKernel-User we catch all the exceptions and signals that the code might produce. One should judiciously relax these requirements in a language that is presented to the programmer, and allow re-raising and re-sending clauses to be automatically inserted.

Equational theory
We present λ coop as an equational calculus, i.e., the interactions between its components are described by equations. Such a presentation makes it easy to reason about program equivalence. There are three equality judgements It is presupposed that we only compare well-typed expressions with the indicated types. For the most part, the context and the type annotation on judgements will play no significant role, and so we shall drop them whenever possible. We comment on the computational equations for constructs characteristic of λ coop , and refer the reader to the online appendix for other equations. When read left-to-right, these equations explain the operational meaning of programs.
Of the three equations for run, the first two specify that returned values and raised exceptions are handled by the corresponding clauses, where F def " treturn x @ c Þ Ñ N, praise e @ c Þ Ñ N e q ePE , pkill s Þ Ñ N s q sPS u. The third equation below relates running an operation op with executing the corresponding co-operation K op , where R stands for the runner tpop x Þ Ñ K op q opPΣ u C : using R @ W run pop X pV, px . M q, pN 1 e 1 q e 1 PEop qq finally F " kernel K op rV {xs @ W finally return x @ c 1 Þ Ñ pusing R @ c 1 run M finally F q, raise e 1 @ c 1 Þ Ñ pusing R @ c 1 run N 1 e 1 finally F q˘e 1 PEop , pkill s Þ Ñ N s q sPS ( Because K op is kernel code, it is executed in kernel mode, whose finally clauses specify what happens afterwards: if K op returns a value, or raises an exception, execution continues with a suitable continuation, with R wrapped around it; and if K op sends a signal, the corresponding finalisation code from F is evaluated. The next bundle describes how kernel code is executed within user code: kernel praise X@C eq @ W finally F " N e rW {cs, We also have an equation stating that an operation called in kernel mode propagates out to user mode, with its continuations wrapped in kernel mode: Similar equations govern execution of user computations in kernel mode. The remaining equations include standard βη-equations for exception handling [7], deconstruction of products and sums, algebraicity equations for operations [33], and the equations of kernel theory from §3.1, describing how getenv and setenv work, and how they interact with signals and other operations.

Denotational semantics
We provide a coherent denotational semantics for λ coop , and prove it sound with respect to the equational theory given in §4. 4. Having eschewed all forms of recursion, we may afford to work simply over the category of sets and functions, while noting that there is no obstacle to incorporating recursion at all levels and switching to domain theory, similarly to the treatment of effect handlers in [3].

Semantics of types
The meaning of terms is most naturally defined by structural induction on their typing derivations, which however are not unique in λ coop due to subsumption rules. Thus we must worry about devising a coherent semantics, i.e., one in which all derivations of a judgement get the same meaning. We follow prior work on the semantics of effect systems for handlers [3], and proceed by first giving a skeletal semantics of λ coop in which derivations are manifestly unique because the effect information is unrefined. We then use the skeletal semantics as the frame upon which rests a refinement-style coherent semantics of the effectful types of λ coop .
The skeletal types are like λ coop 's types, but with all effect information erased. In particular, the ground types A, and hence the kernel state types C, do not change as they contain no effect information. The skeletal value types are P, Q ::" A | unit | empty | PˆQ | P`Q | P Ñ Q! | P Ñ Q C | runner C.
The skeletal versions of the user and kernel types are P ! and P C, respectively. It is best to think of the skeletal types as ML-style types which implicitly over-approximate effect information by "any effect is possible", an idea which is mathematically expressed by their semantics, as explained below.
First of all, the semantics of ground types is straightforward. One only needs to provide sets denoting the base types b, after which the ground types receive the standard set-theoretic meaning, as given in Fig. 4.
Recall that O, S, and E are the sets of all operations, signals, and exceptions, and that each op P O has a signature op : A op B op ! E op . Let us additionally assume that there is a distinguished operation O P O with signature O : 1 0 ! 0 (otherwise we adjoin it to O). It ensures that the denotations of skeletal user and kernel types are pointed sets, while operationally O indicates a runtime error.
Next, we define the skeletal user and kernel monads as and and raises no exceptions. We prefer the former, as it reflects our treatment of exceptions as a control mechanism rather than exceptional values.
These ingredients suffice for the denotation of skeletal types as sets, as given in Fig. 4. The user and kernel skeletal types are interpreted using the respective skeletal monads, and hence the two function types as Kleisli exponentials.
We proceed with the semantics of effectful types. The skeleton of a value type X is the skeletal type X s obtained by removing all effect information, and similarly for user and kernel types, see Fig. 5. We interpret a value type X as a subset rrrXsss Ď rrX s ss of the denotation of its skeleton, and similarly for user and computation types. In other words, we treat the effectful types as refinements of their skeletons. For this, we define the operation pX 0 , X 1 q pY 0 , Y 1 q, for any X 0 Ď X 1 and Y 0 Ď Y 1 , as the set of maps X 1 Ñ Y 1 restricted to X 0 Ñ Y 0 : Next, observe that the user and the kernel monads preserve subset inclusions, in the sense that U Σ,E X Ď U Σ 1 ,E 1 X 1 and K Σ,E,S,C X Ď K Σ 1 ,E 1 ,S 1 ,C X 1 if Σ Ď Σ 1 , E Ď E 1 , S Ď S 1 , and X Ď X 1 . In particular, we always have U Σ,E X Ď U s X and K Σ,E,S,C X Ď K s C X. Finally, let Runner Σ,Σ 1 ,S C Ď Runner s C be the subset of those runners R whose co-operations for Σ factor through K Σ 1 ,Eop,S,C , i.e., op R : rrA op ss Ñ K Σ 1 ,Eop,S,C rrB op ss Ď K O,Eop,S,C rrB op ss, for each op P Σ. Semantics of effectful types is given in Fig. 5. From a category-theoretic viewpoint, it assigns meaning in the category SubpSetq whose objects are subset inclusions X 0 Ď X 1 and morphisms from X 0 Ď X 1 to Y 0 Ď Y 1 those maps X 1 Ñ Y 1 that restrict to X 0 Ñ Y 0 . The interpretations of products, sums, and function types are precisely the corresponding category-theoretic notionsˆ,`, and in SubpSetq. Even better, the pairs of submonads U Σ,E Ď U s and K Σ,E,S,C Ď K s C are the "SubpSetq-variants" of the user and kernel monads. Such an abstract point of view drives the interpretation of terms, given below, and it additionally suggests how our semantics can be set up on top of a category other than Set. For example, if we replace Set with the category Cpo of ω-complete partial orders, we obtain the domain-theoretic semantics of effect handlers from [3] that models recursion and operations whose signatures contain arbitrary types.

Semantics of values and computations
To give semantics to λ coop 's terms, we introduce skeletal typing judgements Γ $ s V : P, Γ $ s M : P !, Γ $ s K : P C, which assign skeletal types to values and computations. In these judgements, Γ is a skeletal context which assigns skeletal types to variables. The rules for these judgements are obtained from λ coop 's typing rules, by excluding subsumption rules and by relaxing restrictions on effects. For example, the skeletal versions of the rules TyValue-Runner and TyKernel-Kill arè The relationship between effectful and skeletal typing is summarised as follows: (1) Skeletal typing derivations are unique. (2) If X Ď Y , then X s " Y s , and analogously for subtyping of user and kernel types. (3) If Γ $ V : X, then Γ s $ s V : X s , and analogously for user and kernel computations. Proof. We prove (1) by induction on skeletal typing derivations, and (2) by induction on subtyping derivations. For (1), we further use the occasional type annotations, and the absence of skeletal subsumption rules. For proving (3), suppose that D is a derivation of Γ $ V : X. We may translate D to its skeleton D s deriving Γ s $ s V : X s by replacing typing rules with matching skeletal ones, skipping subsumption rules due to (2). Computations are treated similarly.
Provided maps rrA 1 ssˆ¨¨¨ˆrrA n ss Ñ rrBss denoting ground constants f, values are interpreted in a standard way, using the bi-cartesian closed structure of sets, except for a runner tpop x Þ Ñ K op q opPΣ u C , which is interpreted at an environment γ P rrΓ ss as the skeletal runner top : rrA op ss Ñ K O,Eop,S,rrCss rrB op ssu opPO , given by op a def " pif op P Σ then ρprrΓ, x : A op $ s K op : B op Csspγ, aqq else Oq.
The purpose of O in the definition of op is to model a runtime error when the runner is asked to handle an unexpected operation, while ρ makes sure that op raises at most the exceptions E op , as prescribed by the signature of op.
User and kernel computations are interpreted as elements of the corresponding skeletal user and kernel monads. Again, most constructs are interpreted in a standard way: returns as the units of the monads; the operations raise, kill, getenv, setenv, and ops as the corresponding algebraic operations; and match statements as the corresponding semantic elimination forms. The interpretation of exception handling offers no surprises, e.g., as in [30], as long as we follow the strategy of treating unexpected situations with the runtime error O.
The most interesting part of the interpretation is the semantics of where F def " treturn x @ c Þ Ñ N, praise e @ c Þ Ñ N e q ePE , pkill s Þ Ñ N s q sPS u. At an environment γ P rrΓ ss, V is interpreted as a skeletal runner with state rrCss, which induces a monad morphism r : Tree O p´q Ñ prrCss ñ Tree O p´ˆrrCss`Sqq, as in the proof of Prop. 3. Let f : K s rrCss rrP ss Ñ prrCss ñ U s rrQssq be the skeletal kernel theory homomorphism characterised by the equations f preturn pq " λc . rrΓ, x : P, c : C $ s N : Qsspγ, p, cq, f poppa, κ, pν e q ePEop qq " λc . oppa, λb . f pκ bq c, pf pν e q cq ePEop q, f praise eq " λc . pif e P E then rrΓ, c : C $ s N e : Qsspγ, cq else Oq, f pkill sq " λc . pif s P S then rrΓ $ s N s : Qss γ else Oq, The interpretation of (4) at γ is f pr rrP ss`E prrΓ $ s M : P !ss γqq prrΓ $ s W : Css γq, which reads: map the interpretation of M at γ from the skeletal user monad to the skeletal kernel monad using r (which models the operations of M by the cooperations of V ), and from there using f to a map rrCss ñ U s rrQss, that is then applied to the initial kernel state, namely, the interpretation of W at γ. We interpret the context switch Γ $ s kernel K @ W finally F : Q! at an environment γ P rrΓ ss as f prrΓ $ s K : P Css γq prrΓ $ s W : Css γq, where f is the map (5). Finally, user context switch is interpreted much like exception handling.
We now define coherent semantics of λ coop 's typing derivations by passing through the skeletal semantics. Given a derivation D of Γ $ V : X, its skeleton D s derives Γ s $ s V : X s . We identify the denotation of V with the skeletal one, rrrΓ $ V : Xsss def " rrΓ s $ s V : X s ss : rrΓ s ss Ñ rrX s ss.
All that remains is to check that rrrΓ $ V : Xsss restricts to rrrΓ sss Ñ rrrXsss. This is accomplished by induction on D. The only interesting step is subsumption, which relies on a further observation that X Ď Y implies rrrXsss Ď rrrY sss. Typing derivations for user and kernel computations are treated analogously.

Coherence, soundness, and finalisation theorems
We are now ready to prove a theorem that guarantees execution of finalisation code. But first, let us record the fact that the semantics is coherent and sound.
Theorem 6 (Coherence and soundness). The denotational semantics of λ coop is coherent, and it is sound for the equational theory of λ coop from §4.4.
Proof. Coherence is established by construction: any two derivations of the same typing judgement have the same denotation because they are both (the same) restriction of skeletal semantics. For proving soundness, one just needs to unfold the denotations of the left-and right-hand sides of equations from §4.4, and compare them, where some cases rely on suitable substitution lemmas.
[ \ To set the stage for the finalisation theorem, let us consider the computation using V @ W run M finally F , well-typed by the rule TyUser-Run from Fig. 3. At an environment γ P rrrΓ sss, the finalisation clauses F are captured semantically by the finalisation map φ γ : prrrXsss`EqˆrrrCsss`S Ñ rrrY ! pΣ 1 , E 1 qsss, given by With φ in hand, we may formulate the finalisation theorem for λ coop , stating that the semantics of using V @ W run M finally F is a computation tree all of whose branches end with finalisation clauses from F . Thus, unless some enveloping runner sends a signal, finalisation with F is guaranteed to take place.
Proof. We first prove that f u c " φ : γ pu cq holds for all u P K Σ 1 ,E,S,rrrCsss rrrXsss and c P rrrCsss, where f is the map (5). The proof proceeds by computational induction on u [29]. The finalisation statement is then just the special case with u def " r rrrXsss`E prrrΓ $ M : X ! pΣ, Eqsss γq and c def " rrrΓ $ W : Csss γ.

Runners in action
Let us show examples that demonstrate how runners can be usefully combined to provide flexible resource management. We implemented these and other examples in the language Coop and a library Haskell-Coop, see §7.
To make the code more understandable, we do not adhere strictly to the syntax of λ coop , e.g., we use the generic versions of effects [26], as is customary in programming, and effectful initialisation of kernel state as discussed in §3.2.
Example 8 (Nesting). In Example 4, we considered a runner fileIO for basic file operations. Let us suppose that fileIO is implemented by immediate calls to the operating system. Sometimes, we might prefer to accumulate writes and commit them all at once, which can be accomplished by interposing between fileIO and user code the following runner accIO, which accumulates writes in its state: { write s' Ñ let s = getenv () in setenv (concat s s') } string By nesting the runners, and calling the outer write (the one of fileIO) only in the finalisation code for accIO, the accumulated writes are commited all at once: using fileIO @ (open "hello.txt") run using accIO @ (return "") run write "Hello, world."; write "Hello, again." finally { return x @ s Ñ write s; return x } finally { return x @ fh Ñ ... , raise QuotaExceeded @ fh Ñ ... , kill IOError Ñ ... } Example 9 (Instrumentation). Above, accIO implements the same signature as fileIO and thus intercepts operations without the user code being aware of it. This kind of invisibility can be more generally used to implement instrumentation: Here the interposed runner implements all operations of some enveloping runner, by simply forwarding them, while also measuring computational cost by counting the total number of operation calls, which is then reported during finalisation.
Example 10 (ML-style references). Continuing with the theme of nested runners, they can also be used to implement abstract and safe interfaces to low-level resources. For instance, suppose we have a low-level implementation of a memory heap that potentially allows unsafe memory access, and we would like to implement ML-style references on top of it. A good first attempt is the runner let (r,h') = malloc h x in setenv h'; return r, get r Ñ let h = getenv () in memread h r, put (r, x) Ñ let h = getenv () in memset h r x } heap which has the desired interface, but still suffers from three deficiencies that can be addressed with further language support. First, abstract types would let us hide the fact that references are just memory locations, so that the user code could never devise invalid references or otherwise misuse them. Second, our simple typing discipline forces all references to hold the same type, but in reality we want them to have different types. This could be achieved through quantification over types in the low-level implementation of the heap, as we have done in the Haskell-Coop library using Haskell's forall. Third, user code could hijack a reference and misuse it out of the scope of the runner, which is difficult to prevent. In practice the problem does not occur because, so to speak, the runner for references is at the very top level, from which user code cannot escape.
Example 11 (Monotonic state). Nested runners can also implement access restrictions to resources, with applications in security [8]. For example, we can restrict the references from the previous example to be used monotonically by associating a preorder with each reference, which assignments then have to obey. This idea is similar to how monotonic state is implemented in the F˚language [2], except that we make dynamic checks where F˚statically uses dependent types.
While we could simply modify the previous example, it is better to implement a new runner which is nested inside the previous one, so that we obtain a modular solution that works with any runner implementing operations ref, get, and put: { mref x rel Ñ let r = ref x in let m = getenv () in setenv (add m (r,rel)); return r, mget r Ñ get r, mput (r, y) Ñ let x = get r in let m = getenv () in match (sel m r) with | inl rel Ñ if (rel x y) then put (r, y) else raise MonotonicityViolation | inr () Ñ kill NoPreoderFound } mappref,intRelq The runner's state is a map from references to preorders on integers. The cooperation mref x rel creates a new reference r initialised with x (by calling ref of the outer runner), and then adds the pair pr, relq to the map stored in the runner's state. Reading is delegated to the outer runner, while assignment first checks that the new state is larger than the old one, according to the associated preorder. If the preorder is respected, the runner proceeds with assignment (again delegated to the outer runner), otherwise it reports a monotonicity violation. We may not assume that every reference has an associated preorder, because user code could pass to mput a reference that was created earlier outside the scope of the runner. If this happens, the runner simply kills the offending user code with a signal.
Example 12 (Pairing). Another form of modularity is achieved by pairing runners. Given two runners tpop x Þ Ñ K op q opPΣ1 u C1 and tpop 1 x Þ Ñ K op 1 q op 1 PΣ2 u C2 , e.g., for state and file operations, we can use them side-by-side by combining them into a single runner with operations Σ 1`Σ2 and kernel state C 1ˆC2 , as follows (the co-operations op 1 of the second runner are treated symmetrically): { op x Ñ let (c,c') = getenv () in user kernel (Kop x) @ c finally { return y @ c'' Ñ return (inl (inl y, c'')), (raise e @ c'' Ñ return (inl (inr e, c'')))ePE op , (kill s Ñ return (inr s))sPS 1 } with { return (inl (inl y, c'')) Ñ setenv (c'', c'); return y, return (inl (inr e, c'')) Ñ setenv (c'', c'); raise e, return (inr s) Ñ kill s}, op' x Ñ ... , ... }C 1ˆC2 Notice how the inner kernel context switch passes to the co-operation K op only its part of the combined state, and how it returns the result of K op in a reified form (which requires treating exceptions and signals as values). The outer user context switch then receives this reified result, updates the combined state, and forwards the result (return value, exception, or signal) in unreified form.

Implementation
We accompany the theoretical development with two implementations of λ coop : a prototype language Coop [6], and a Haskell library Haskell-Coop [1].
Coop, implemented in OCaml, demonstrates what a more fully-featured language based on λ coop might look like. It implements a bi-directional variant of λ coop 's type system, extended with type definitions and algebraic datatypes, to provide algorithmic typechecking and type inference. The operational semantics is based on the computation rules of the equational theory from §4.4, but extended with general recursion, pairing of runners from Example 12, and an interface to the OCaml runtime called containers-these are essentially top-level runners defined directly in OCaml. They are a modular and systematic way of offering several possible top-level runtime environments to the programmer.
The Haskell-Coop library is a shallow embedding of λ coop in Haskell. The implementation closely follows the denotational semantics of λ coop . For instance, user and kernel monads are implemented as corresponding Haskell monads. Internally, the library uses the Freer monad of Kiselyov [14] to implement free model monads for given signatures of operations. The library also provides a means to run user code via Haskell's top-level monads. For instance, code that performs input-output operations may be run in Haskell's IO monad.
Haskell's advanced features make it possible to use Haskell-Coop to implement several extensions to examples from §6. For instance, we implement ML-style state that allow references holding arbitrary values (of different types), and state that uses Haskell's type system to track which references are alive. The library also provides pairing of runners from Example 12, e.g., to combine state and input-output. We also use the library to demonstrate that ambient functions from the Koka language [18] can be implemented with runners by treating their binding and application as co-operations. (These are functions that are bound dynamically but evaluated in the lexical scope of their binding.)

Related work
Comodels and (ordinary) runners have been used as a natural model of stateful top-level behaviour. For instance, Plotkin and Power [27] have given a treatment of operational semantics using the tensor product of a model and a comodel. Recently, Katsumata, Rivas, and Uustalu have generalised this interaction of models and comodels to monads and comonads [13]. An early version of Eff [4] implemented resources, which were a kind of stateful runners, although they lacked satisfactory theory. Uustalu [35] has pointed out that runners are the additional structure that one has to impose on state to run algebraic effects statefully. Møgelberg and Staton's [21] linear-use state-passing translation also relies on equipping the state with a comodel structure for the effects at hand. Our runners arise when their setup is specialised to a certain Kleisli adjunction.
Our use of kernel state is analogous to the use of parameters in parameterpassing handlers [30]: their return clause also provides a form of finalisation, as the final value of the parameter is available. There is however no guarantee of finalisation happening because handlers need not use the continuation linearly.
The need to tame the excessive generality of handlers, and willingness to give it up in exchange for efficiency and predictability, has recently been recognised by Multicore OCaml's implementors, who have observed that in practice most handlers resume continuations precisely once [9]. In exchange for impressive efficiency, they require continuations to be used linearly by default, whereas discarding and copying must be done explicitly, incurring additional cost. Leijen [17] has extended handlers in Koka with a finally clause, whose semantics ensures that finalisation happens whenever a handler discards its continuation. Leijen also added an initially clause to parameter-passing handlers, which is used to compute the initial value of the parameter before handling, but that gets executed again every time the handler resumes its continuation.

Conclusion and future work
We have shown that effectful runners form a mathematically natural and modular model of resources, modelling not only the top level external resources, but allowing programmers to also define their own intermediate "virtual machines". Effectful runners give rise to a bona fide programming concept, an idea we have captured in a small calculus, called λ coop , which we have implemented both as a language and a library. We have given λ coop an algebraically natural denotational semantics, and shown how to program with runners through various examples.
We leave combining runners and general effect handlers for future work. As runners are essentially affine handlers, inspired by Multicore OCaml we also plan to investigate efficient compilation for runners. On the theoretical side, by developing semantics in a SubpCpoq-enriched setting [32], we plan to support recursion at all levels, and remove the distinction between ground and arbitrary types. Finally, by using proof-relevant subtyping [34] and synthesis of lenses [20], we plan to upgrade subtyping from a simple inclusion to relating types by lenses.