Doo bee doo bee doo

Abstract We explore the design and implementation of Frank, a strict functional programming language with a bidirectional effect type system designed from the ground up around a novel variant of Plotkin and Pretnar’s effect handler abstraction. Effect handlers provide an abstraction for modular effectful programming: a handler acts as an interpreter for a collection of commands whose interfaces are statically tracked by the type system. However, Frank eliminates the need for an additional effect handling construct by generalising the basic mechanism of functional abstraction itself. A function is but the special case of a Frank operator that interprets no commands. Moreover, Frank’s operators can be multihandlers which simultaneously interpret commands from several sources at once, without disturbing the direct style of functional programming with values. Effect typing in Frank employs a novel form of effect polymorphism which avoids mentioning effect variables in source code. This is achieved by propagating an ambient ability inwards, rather than accumulating unions of potential effects outwards. With the ambient ability describing the effects that are available at a certain point in the code, it can become necessary to reconfigure access to the ambient ability. A primary goal is to be able to encapsulate internal effects, eliminating a phenomenon we call effect pollution. Moreover, it is sometimes desirable to rewire the effect flow between effectful library components. We propose adaptors as a means for supporting both effect encapsulation and more general rewiring. Programming with effects and handlers is in its infancy. We contribute an exploration of future possibilities, particularly in combination with other forms of rich type systems.


Introduction
Shall I be pure or impure?
-Philip Wadler (1992) We say "Yes": purity is a choice to make locally. We introduce Frank, an applicative language where the meaning of "impure" computations is open to negotiation, based on Plotkin and Pretnar's effect handlers (Plotkin & Pretnar, 2013)-a rich foundation for effectful programming. By separating effect interfaces from their implementation, effect handlers offer a high degree of modularity. Programmers can express effectful programs independently of the concrete interpretation of their effects. A handler gives one interpretation of the effects of a computation. In Frank, effect types (sometimes called simply effects in the literature) are known as abilities. An ability denotes the permission to invoke a particular set of commands.
Frank programs are written in direct style in the spirit of effect type systems (Lucassen & Gifford, 1988;Talpin & Jouvelot, 1994). Frank operators generalise call-by-value functions in two dimensions. First, operators handle effects. A unary operator is an effect handler, acting as an interpreter for a specified set of commands whose types are statically tracked by the type system. A unary function is but the special case of a unary operator whose handled command set is empty. Second, operators are n-ary, handling multiple computations over distinct command sets simultaneously. An n-ary function is but the special case of an n-ary operator whose handled command sets are all empty.
The contributions of this paper are: • the definition of Frank, a strict functional programming language featuring a bidirectional effect type system, effect polymorphism, and effect handlers; • operators as both multihandlers for handling multiple computations over distinct effect sets simultaneously and as functions acting on values; • a novel approach to effect polymorphism which avoids mentioning effect variables in source code, crucially relying on the observation that one must always instantiate the effects of an operator being applied with the ambient ability, that is, precisely those effects permitted by the current typing context; • a small-step operational semantics for Frank and a proof of type soundness; • a flexible way of reconfiguring access to the ambient ability via adaptors, thereby eliminating the phenomenon of effect pollution and facilitating composition of effectful components; • an extended example making essential use of adaptors in composing handlers together to implement concurrent actors with mailboxes; • an exploration of directions for future research, combining effect-and-handlers programming with features including substructural typing, dependent types, and totality.
A number of other languages and libraries are built around effect handlers. Bauer and Pretnar's Eff (Bauer & Pretnar, 2015) language is an ML-like language extended with effect handlers. A significant difference between Frank and the original version of Eff is that the latter provides no support for effect typing. Recently, Bauer and Pretnar data Zero = data Unit = unit data Bool = tt | ff data Nat = zero | suc Nat data List X = nil | cons X ( List X) data Pair X Y = pair X Y The data types Int and Char are built in and String is an alias for List Char. There is syntactic sugar for lists, for instance: We choose to treat constructors as distinct from functions, and constructors must always be fully applied. We can write perfectly ordinary first-order functional programs by pattern matching. Type signatures are compulsory, universally quantifying implicitly over freely occurring type variables and insisting on the corresponding parametric polymorphism.

Effect polymorphism in ambient silence
Computations, such as functions, have computation types, which embed explicitly into the value types: braces play the role of "suspenders" in types and values. Accordingly, we can write typical higher-order functions map : {{ X -> Y} -> List X -> List Y} map f nil = nil map f (x :: xs ) = (f x) :: ( map f xs ) and apply them in the usual way: map {n -> n +1} [1 , 2, 3] =⇒ [2 , 3, 4] A value type A is a data type D R 1 ... R n , a suspended computation type {C}, or a type variable X. A computation type T 1 -> ... -> T m -> [I 1 , ..., I n ]B resembles a function type with m argument types and a return type showing the ability the computation needs-a bracketed list of n interface instances-and the value type it delivers. In Frank, names always bind values (a simplifying decision which we shall re-examine in Section 10). Top-level definitions give names to suspended computations.
Typechecking separates cleanly into checking the compatibility of value types and checking that required abilities are available. Empty abilities may be omitted but take a care as this does not denote purity. Instead, the definition map : {{ X -> Y} -> List X -> List Y} is really a short hand for map : {{ X -> [ε |] Y} -> List X -> [ε |] List Y} which makes the map operator implicitly effect-polymorphic in the effect variable ε. The type of map in Frank says that whatever ability an instance receives will be offered in turn to the operator that acts on each element. That is, we have written something like ML's map but without giving up control over effects, and we have written something like Haskell's map but acquired a function as general as its monadic mapM, as we shall see just as soon as we acquire nontrivial ability.

Controlling evaluation
Frank is a (left-to-right) call-by-value language, so we should be careful when defining control operators. For instance, we may define sequential composition operators fst : {X -> Y -> X} snd : {X -> Y -> Y} fst x y = x snd x y = y Both arguments are evaluated (relative to the ambient ability), before one value is returned. We take the liberty of writing snd x y as x; y, echoing the ML semicolon, and note its associativity.
Meanwhile, avoiding evaluation must be done explicitly by suspending computations. The following operator: iffy : { Bool -> X -> X -> X} iffy tt t f = t iffy ff t f = f is the conditional expression operator which forces evaluation of the condition and both branches, before choosing between the values. To write the traditional conditional, we must therefore suspend the second and third arguments: Again, Frank variables stand for values, but t and f are not values of type X. Rather, they are suspended computations of type {X}, but we must do just one. The postfix ! denotes nullary application of a suspended computation.
We write suspended computations in braces, with a choice of zero or more pattern matching clauses separated by | symbols. In a nullary suspension, we have one choice, which is just written as an expression in braces, for instance, if fire ! { launch missiles } { unit } assuming that launch is a command permitted by the ambient ability, granted to both branches by the silently effect-polymorphic type of if.
With non-nullary suspensions, we can define case-expressions inline using reverse application

Abilities collect interfaces; Interfaces offer commands
Abilities (Frank's realisation of effects) are collections of instantiated interfaces, each of which describes a choice of commands (known elsewhere as operations; Plotkin & Pretnar, 2013). Here are some simple interface definitions.
interface Send X = send : X -> Unit interface Receive X = receive : X interface State S = get : S | put : S -> Unit The send command takes an argument of type X and returns a value of type Unit. The receive command returns a value of type X. The State interface offers get and put commands. Note that, unlike data constructors, commands are first-class values. In particular, while Zero is uninhabited, {[Receive Int]Zero} contains the value receive!. We may use the silent effect polymorphism of map nontrivially to send a list of elements, one at a time: sends : { List X -> [ Send X] Unit } sends xs = map send xs ; unit The reason this type checks at all is because map is implicitly polymorphic in its effects. The bracket [Send X] demands that the ambient ability permits at least the Send X commands.
Doo bee doo bee doo 7 The type of map works with any ambient ability, hence certainly with those permitting Send X, and it passes that ability to its computation argument, which may thus be send.
However, the following does not typecheck, because Send X has not been included in the return type of bad.
bad : { List X -> Unit } bad xs = map send xs ; unit There is no effect inference in Frank. The typing rules' conclusions do not accumulate the abilities of the programs in their premisses. Rather, we are explicit about what the environment makes possible-via the ambient ability-and where and how that changes.
In designing Frank, we have sought to maintain the benefits of effect polymorphism while avoiding the need to write effect variables in source code. There are no explicit effect variables in any of the examples in this paper. In an earlier version of Frank, we ruled out explicit effect variables by fiat. But doing so placed artificial restrictions on the formalism (see Section 4), so we do now permit them. A case where explicit effect variables may be useful is in manipulating data types containing multiple suspended computations with different abilities; we are yet to explore compelling use cases.

Polymorphic commands
The original version of Frank  supports only monomorphic commands. We have extended it with polymorphic commands (Kammar et al., 2013) which can be invoked at any type.
interface Abort = abort X : X The abort command is suitable for expressing abortion of a computation: it can be invoked anywhere as it produces a value of any type X and it is guaranteed to end computation as there is no way to produce a value for a generic X. Previously, we had to do some gymnastics to get the same effect. The postfix ! attached to abort denotes the definition of a nullary operator. The abort' operator simulates the abort command by requesting a value of type Zero, which guarantees the end of computation as there is no way to produce such a value. Furthermore, polymorphic commands are necessary for more interesting examples like ML-style dynamically allocated references.
ML-style References. Based on polymorphic commands, the Frank implementation now supports a special top-level effect for ML-style dynamically allocated references.
Both the data type Ref X and the interface RefState are built-in, as Frank is not yet expressive enough to define them in the language. We give an example in Section 5 of how built-in effects are declared in the main program to be handled externally.

Direct style for monadic programming
We work in a direct applicative style. When the output of one computation is used as the input to another, we may just write an application, or a case analysis, directly. For instance, we can implement the result of repeatedly reading lists until one is empty and concatenating the result. The latter part of catter could perhaps be written without naming ys as (xs ++) <$> catter, or even, with "idiom brackets", (|pure xs ++ catter|), but always there is extra plumbing (here do-notation and return) whose only purpose is to tell the compiler where to parse a type as effect value and where just as value. The choice to be frank about the separation of effects from values in the syntax of types provides a stronger cue to the status of each component and reduces the need for plumbing. We do not, however, escape the need to disambiguate doing receive! from being receive.
In the same mode, we can implement the C++ "increment c, return original value" operation as follows: In Haskell, next would have to be explicitly sequentialised.
next :: State Int Int next = do x <-get y <-get put y +1 return x (We have written get twice to match the preceding Frank code, but assuming a standard implementation of state one could of course delete the second get and use x in place of y.) The absence of explicit plumbing in Frank depends crucially on the fact that Frank, unlike Haskell, has a fixed evaluation order.
Doo bee doo bee doo 9

Handling by application
In a call-by-value language, a function application can be seen as a particularly degenerate mode of coroutining between the function and its argument. The function process waits while the argument process computes to a value, transmitted once as the argument's terminal action; on receipt, the function post-processes that value in some way, before transmitting its value in turn. An exception handler is also a means for one process to contextualise the execution of another, remaining entirely dormant and dismounting itself without fuss if its subordinate runs without failure, but stepping into the breach if needs be. Pascal's "with. . . do. . . " contextualises the "do" part by bringing into scope the fields of the record given by the "with" (Jensen & Wirth, 1974). One of the third author's early programming languages, from 1991, extends LOGO (Papert, 1980) with a first class and compositional notion of context, generalising functions, records, exception handlers, and localised remapping-handling, in today's terminology-of the "turtle graphics" commands, to support mathematical investigations by diagram transformation (McBride, 2011). Its syntax for contextualisation is exactly the blank space of application, and it is from that early experience that we take elements of our present design.
Frank is already distinct from other languages with effect handlers in its effect type system, but the key departure it makes in program style is to handle effects without any special syntax for invoking an effect handler. Rather, the ordinary notion of "function" is extended with the means to offer effects to arguments, invoked just by application. That is, the blank space signifies more general modes of coroutining between operator and arguments than the return-value-then-quit default. For instance, the usual behaviour of the "state" commands can be given as follows: state : {S -> < State S >X -> X} state _ x = x state s < get -> k > = state s (k s) state _ < put s -> k > = state s (k unit ) Let us give an example using state before unpacking its definition. We might pair the elements of a list with successive numbers.
index : { List X -> List ( Pair Int X)} index xs = state 0 ( map {x -> pair next ! x} xs ) Allowing string notation for lists of characters, we obtain: What is happening?
The type of state shows us that Frank operators do not merely have argument value types but also an adjustment to the ambient ability, written in chevrons and usually omitted when it is the identity (as in all of the examples we have seen so far). Whatever the ambient ability might be when state is invoked, the initial state should arrive at its first argument using only that ability; the ambient ability at its second argument will include the State S interface, shadowing any other State A interfaces which might have been present already. Correspondingly, by the time index invokes map, the ambient ability includes State Int, allowing the elementwise operation to invoke next!.
The first equation of state explains what to do if any value is passed as the second argument. In Frank, a traditional pattern built from constructors and variables matches only values, so the x is not a catch-all pattern, as things other than values can be passed as that argument. In particular, requests can be passed as the second argument, in accordance with the State S interface. A request consists of a command instance and a continuation. Requests are matched by patterns in chevrons which show the particular command instance being handled left of ->, with a pattern variable standing for the continuation on the right. The patterns of state thus cover all possible signals (that is, values or requests) advertised as acceptable at its arguments.
Having received signals for each argument the state operator should handle them. If the second argument is a value, then that value is returned. If the second argument is a request, then the state operator is reinvoked with a new state (which is simply the old state in the case of get and s in the case of put s) in the first argument and the continuation invoked in the second argument.
We emphasise a key difference between Frank and most other languages supporting effect handlers (including Eff, Helium, Koka, and Multicore OCaml Dolan et al., 2015): Frank's continuation variables are shallow in that they capture only the rest of the subordinated computation, not the result of handling it, allowing us to change how we carry on handling, for instance, by updating the state. In contrast, Multicore OCaml's continuation variables are deep in that invoking them implicitly reinvokes the handler. Consider the definition for state in Multicore OCaml effect Put : t -> unit let put x = perform ( Put x) effect Get : t let get () = perform Get let state m = match m () with | x -> fun s -> x | effect Get k -> fun s -> continue k s s | effect ( Put s) k -> fun _ -> continue k () s Multicore OCaml provides three special keywords for effects and handlers: effect declares a command or marks a request pattern, perform invokes a command, and continue invokes a continuation. The shallow implementation of state in Frank requires explicit recursion. The deep implementation of state in Multicore OCaml performs the recursion implicitly. On the other hand, the shallow version allows us to thread the state through the operator, whereas the deep version relies on interpreting a stateful computation as a function and threading the state through the continuation.
Shallow handlers can straightforwardly express deep handlers using explicit recursion. Deep handlers can encode shallow handlers in much the same way that iteration (catamorphism, fold) can encode primitive recursion (paramorphism) and with much the same increase in complexity. On the other hand, handlers which admit a deep implementation have a more regular behaviour and admit easier reasoning, just as "folds" offer specific proof techniques not available to pattern matching programs in general. Kammar et al. (2013) provide a more in-depth discussion of the trade-offs between deep and shallow handlers.

Handling multiple arguments at once
Frank allows the programmer to write n-ary operators, so we can offer different adjustments to the ambient ability at different arguments. For instance, we can implement a pipe operator which matches receive commands downstream with send commands upstream.
pipe : {< Send X > Unit -> < Receive X >Y -> [ Abort ]Y} pipe < send x -> s > < receive -> r > = pipe (s unit ) (r x) pipe <_ > y = y pipe unit <_ > = abort ! The type signature conveys several different things. The pipe operator must handle all commands from Send X on its first argument and all commands from Receive X on its second argument. We say that pipe is thus a multihandler. The value of the first argument has type Unit and the value of the second argument has type Y. The operator itself is allowed to perform Abort commands and returns a final value of type Y. The first line implements the communication between producer and consumer, reinvoking pipe with both continuations, giving the sent value to the receiver. The second line makes use of the catch-all pattern <_> which matches either a send command or an attempt to return a value: as the consumer has delivered a value the producer can be safely discarded. The third line covers the case which falls through: the catch-all pattern must be a receive command, as the value case has been treated already, but the producer has stopped sending, so abort is invoked to indicate a "broken pipe".
pipe ( pipe ( sends [" do ", " be ", ""]) spacer !) catter ! =⇒ " do be " There is nothing you can do with simultaneous handling that you cannot also do with mutually recursive handlers for one process at a time. The Frank approach is, however, more direct. Kammar et al. (2013) provide both deep and shallow handler implementations for pipes using their Haskell effects library. Both implementations are significantly more complex than the above definition in Frank, requiring the unary handler for sending (receiving) to maintain a suspended computation to the consumer (producer) to continue the interaction upon receipt of a command. Moreover, the deep handler implementation depends on a nontrivial mutually recursive data type, which places considerable cognitive load on the programmer. So, even in systems such as Multicore OCaml and Eff, offering a more aesthetic syntax than Kammar et al.'s library, a programming burden remains.
Let us clarify that the adjustment marked in chevrons in an argument type promises exactly what will be handled for that argument. The return type of pipe requires the ambient ability to support Abort, and its arguments offer to extend that ability with Send X and Receive X, respectively, so the producer and consumer will each also support Abort. However, because neither argument type advertises Abort in its adjustment, the implementation of pipe may not intercept the aborting command. In particular, the catch-all pattern <_> matches only the signals advertised by the relevant argument type, with other commands forwarded transparently to the most local argument offering the relevant interface. No Frank process may secretly intercept commands. Of course, the pipe operator can prevent action by ignoring the continuation to a send on its first argument or a receive on its second, but it cannot change the meaning of other commands.
One can simulate adjustments using a more conventional effect type system with abilities on both argument types and return types. However, this yields more verbose and less precise types. For instance, the type of the first argument to pipe becomes <Abort, Send X>Unit instead of <Send X>Unit. The argument type has been polluted by the ability of the return type and it now fails to advertise precisely which effects it handles and which effects it forwards untouched.

The catch question
Frank allows us to implement an "exception handler" with a slightly more nuanced type than is sometimes seen.
The first argument to catch is the computation to run that may raise an exception. The second argument is the alternative computation to run in the case of failure, given as a suspended computation allowing us to choose whether to run it. We do not presume that the ambient ability in which catch is executed offers the Abort interface. In contrast, a typical treatment of exceptions renders catch as the prioritised choice between two failure-prone computations. For instance, the Haskell mtl library offers where the exception handler is always allowed to throw an error. In other words, this Haskell typing unnecessarily makes the ability to abort non-local. Leijen makes a similar observation in Koka's treatment of exceptions (Leijen, 2014).
Frank's effect polymorphism ensures that the alternative computation is permitted to abort if and only if catch is, so we lose no functionality but gain precision. Moreover, we promise that catch will trap abort only in its first argument, so that any failure (or anything else) that h! does is handled by the environment-indeed, you can see that h! is executed as a tail call, if at all, thus outside the scope of catch. In the case that the ambient ability is allowed to abort, then when the adjustment is applied to the ambient ability we obtain an ability with two instances of the Abort interface. Frank allows duplicate instances and respects their order, i.e. an ability is a map from interfaces to lists of interface instances. When invoking a command, it is always directed at the rightmost interface instance, i.e. the one most recently added.

The disappearance of control
Using one of the many variations on the theme of free monads, we could implement operators like state, pipe, and catch as abstractions over computations reified as commandresponse trees. By contrast, our handlers do not abstract over computations, nor do they have computation-to-computation handler types distinct from value-to-computation function types (Kammar et al., 2013;Bauer & Pretnar, 2014).
Frank computations are abstract: a thing of type {C} can be communicated or invoked but not inspected. Argument types explain which values are expected, and operators match on those values directly, without apparently forcing a computation, yet they also admit other specific modes of interaction, handled in specific ways.
Semantically, then, a Frank operator must map computation trees to computation trees, but we write its action on values directly and its handling of commands minimally. The machinery by which commands from the input not handled locally must be forwarded with suitably wrapped continuations is hard-wired, as we shall make explicit in Section 4.3.

Adapting effects
The measure of intelligence is the ability to change.
-often attributed to Albert Einstein In this section, we motivate the need for a way of reconfiguring access to the ambient ability. We start by discussing the problem of effect pollution and point out how this problem has been solved in the literature so far. Then, we propose the more general solution of adaptors which support arbitrary rewiring of effects. We demonstrate the usefulness of the added generality on an example that requires effect duplication and effect swapping. Lastly, we motivate and present adaptor adjustments, which are adaptors that allow the precise specification of effect flow at the type-level.

Effect Pollution.
We present a small example in order to illustrate a problem that can occur on operator composition. Consider the following two operators: The maybe handler results in an option type by interpreting an abort command as nothing and a value x as just x. The receives handler, parameterised by a list of state values, interprets a receive command by returning the next value from the list if it exists and by invoking abort if the list is empty. Furthermore, it interprets a value by returning it. It is natural to expect to be able to precompose maybe with receives to obtain an interpretation of the Receive S effect as a pure computation. However, this intended operator was not expressible in earlier versions of Frank. Naive composition yields the following operator: bad : { List S -> < Receive S , Abort >X -> Maybe X} bad ss <m > = maybe ( receives ss m !) As indicated by its type, bad handles both Receive S and Abort. The catch-all pattern <m> binds the second argument to a suspended computation m of type {[Receive S, Abort]X}. As well as handling any abort command raised by the receives handler, it also handles invocations of abort within the argument computation.

bad [1 ,2] ( receive ! + abort !) =⇒ nothing
The second argument to bad raises abort which is intercepted by bad. This is surely not what was intended! Abort effects occurring in the argument computation should be forwarded to the outer context for handling rather than being interpreted by bad. The bad handler has broken abstraction by leaking its interpretation for an empty list of states and revealing such details in the type of its second argument. We call this phenomenon effect pollution. To prevent such effect pollution, we need a way of hiding intermediate effects so that external effects with the same name are not accidentally handled. There are multiple ways to achieve this; alternatives to our approach are discussed in Section 9. The approach we adopt in this paper is a generalisation and adaptation of a construct variously called inject (Leijen, 2014(Leijen, , 2018 and lift (Biernacki et al., 2018) in the literature, which in this paper we call mask. The mask construct allows an intermediate effect to be hidden by explicitly shadowing it. Our generalisation, adaptors, supports the arbitrary remapping of effect names and may appear in types and in terms.

Mask.
Let us return to our example of precomposing maybe with receives, which resulted in bad. Compare bad with the following composed operator good which by use of an adaptor prohibits the unintentional interception of any external Abort effect. The plain Abort adaptor above is an instance of mask, which transforms the ambient ability to obtain the ability for m! by masking out the rightmost instance of Abort providing a means for hiding effects reminiscent of renaming de Bruijn representations for bound names.
Remark. The view of computing the local ability by restricting the ambient ability is natural for a bidirectionally typed language such as Frank, where effect types always flow inwards. An alternative but equivalent view that can be more natural for a language with Hindley-Milner type inference, where effect types flow outwards, is of extending the ability of m! to include a fresh instance of Abort. In this sense, one is injecting (Leijen, 2014) or lifting (Biernacki et al., 2018) the local ability into the ambient ability.
Adaptors as Finite Maps. Frank's adaptors generalise mask to an arbitrary finite map from the effects of the ambient ability to the effects of the supplied computation. Effect instances can be masked (as in good), re-ordered, and duplicated. An adaptor is given by a sequence of adaptor components of the form I(S -> S ) such that each I must be distinct and S, S range over patterns that bind the instances of I. An instance pattern consists of a sequence of variables s a n . . . a 1 , where a 1 , . . . a n bind the first n instances of the interface and s binds the rest (full details are given in Section 4). Examples: The common case of mask omits the map entirely: I is syntactic sugar for I(s a -> s). Technically, mask in combination with handlers is sufficient to express general adaptors, but only through a global transformation. For instance, Biernacki et al. (2018) express swapping of effects using mask (which they call "lift").
Effect Duplication. We have seen how mask solves the problem of effect pollution. We now give two more examples that demonstrate why the general notion of adaptors gives a powerful tool when it comes to composing effectful components. Consider the following two operators: The sqrt operator takes an Int value and computes its square root, exhibiting the Abort effect on negative input. The parseInt operator parses a String to an Int value and may exhibit the Abort effect if the string is malformed. There are two sensible ways of composing these operators. One way is to conflate the two effect instances. Thus, only a single exception handler may be used to handle both kinds of failures. maybe ( conflatedComp " abc ") =⇒ nothing --exc . 1 maybe ( conflatedComp " -16") =⇒ nothing --exc . 2 maybe ( conflatedComp "16") =⇒ just 4 Another way of composing the two operators is to distinguish the two effect instances.
What happens here is that two Abort instances are declared, but the inner one is removed from the ambient ability for the argument position of sqrt. Since parseInt is evaluated in this argument position, effects from parseInt are directed to the remaining Abort instance, which corresponds to the outer one. Here is an example of how the two different failure scenarios can be handled.
We argue that whether to provide conflatedComp or distinctComp is a design choice, possibly taken by the maintainer of some library. Certainly though, distinctComp is more general than conflatedComp. It should therefore be easy to obtain conflatedComp from distinctComp by conflating the two effects, and indeed, it is: In this example, duplication of the Abort effect in the ambient ability allows it to be used for both exception scenarios. Although the reconfiguration of the ambient ability is very simple, the adaptor is quite verbose. In future it may be useful to extend Frank to allow the programmer to give names to adaptors by writing something like Abort(copy) for Abort(s a -> s a a).
Effect Swapping. Consider the composition of the two exception handlers maybe and catch.
The programmer can now combine these components directly which yields a program that is equivalent to the program above, applied to some string s.
Alternatively, adaptors allow the programmer to combine these components with the two Abort instances swapped. As with effect duplication, we might imagine extending Frank to allow the programmer to name such a common adaptor by writing something like Abort(swap) for Abort(s a b -> Adaptor Adjustments. A distinctive feature of Frank is that the effects associated with an operator argument describe an adjustment to the ambient ability. This has two clear benefits over a more conventional type system. First, it is often more parsimonious, as the programmer need only specify the change to the ambient ability rather than the entire ability. Second, it explicitly distinguishes those effects that are handled from those that are forwarded. An unfortunate side-effect of this scheme is that if an interface appears both in the return type and in the argument type then the actual type of an argument computation includes two instances of the interface, which is usually not desirable.
Let us suppose we wish to define an operator that acts as a Receive Int transformer adding one to each integer that is read. We might write the following: which may be combined with another Receive handler without ado: However, suppose we run inc' twice.
For this function to typecheck, we have to insert an extra copy of Receive Int in the argument adjustment. Using an adaptor, we can obtain a type with just one instance of Receive in the adjustment. However, the root cause of the problem is in inc' which still accepts an argument with two copies of Receive Int. Adaptor adjustments are a means for wiring an adaptor into an operator and its type. Consider the following variation: The difference between inc and inc' is that the former applies the Receive adaptor to the ambient ability before applying the rest of the extension Receive Int. As a consequence, the type of an argument includes only one copy of Receive Int and we can compose inc with itself without need for any further inline adaptors.
The general form for an adjustment is | where is an adaptor and is an extension (the part of an adjustment we have seen up to now, which is added to the ambient ability).
The action of an adjustment on an ability is to first apply the adaptor and to then apply the extension. By default we assume the adaptor is the identity and write < > as syntactic sugar for < | >.
When generalising incinc to an operator that composes an arbitrary number of inc operators, the problem of effect pollution becomes even more apparent. Without adaptors, L. Convent et al. it would require an unbounded number of effect instances and is therefore not definable in Frank. With adaptors, we can define such a composed operator as follows:

A Frank formalism
A value is. A computation does.
-Paul Blain Levy (2004) In this section, we give a formal presentation of the abstract syntax and type system of Frank. We formalise the syntax, typing rules, and operational semantics for Frank including adaptor and polymorphic command extensions. (For simplicity, we do not account for built-in effects such as RefState.) The syntax and typing rules extend those of Lindley et al. (2017). Whereas they give an operational semantics via a translation into a core language, our operational semantics is direct, following the first author's master's dissertation (Convent, 2017). We prove a type soundness property (Theorem 7) for the extended system.

Syntax
We sometime use overline notation (M) and sometimes explicit indexing ((M i ) i ) to denote a sequence M 1 , . . . , M n . The types of Frank ( Figure 1) are divided into value types and computation types. Value types are data types D R, suspended computation types {C}, or type variables X . Computation types are built from zero or more argument types T and one return type G. A computation type has argument types 1 | 1 A 1 , . . . , k | k A k and return type [ ]B. A computation of type C must handle effects in i on its i-th argument; all arguments are handled simultaneously. As a result, it returns a value of type B and may perform effects in ambient ability . The i-th argument may perform effects in ambient ability remapped by adaptor i and augmented by extension i . An ability represents an infinite map from interface names I to ordered lists of instances I R 1 , ..., I R n in which finitely many interface names I map to non-empty lists. In the syntax, the order of instances of the same interface is important but the order of instances of different interfaces is not. An ability may be closed (initiated by ∅) or open (initiated by an effect variable E).
An extension represents a finite map from interface names to instances. The extension in an argument type denotes exactly those interfaces which are handled by that argument.
An adaptor represents a finite map from interface names to adaptor components. An adaptor component I(S → S ) specifies a mapping of the ambient ability onto the ability of the argument. It acts only on the ambient ability and not on the extension. An interface pattern S matches a reversed list of interface instances in the ambient ability. s i is the remaining i interface instances in the ambient ability not bound by explicit pattern variables. We usually omit the subscript i when knowing the number of unbound instances is not significant.

Effect Polymorphism with an Invisible Effect Variable.
Consider the type of map in Section 2: Modulo the braces around the function types, this is the same type a functional programmer might expect to write in a language without support for effect typing. In fact, this type desugars into: We adopt the convention that the identity adaptor/extension ι may be omitted from adaptors/extensions and argument types.
Similarly, we adopt the convention that effect variables may be omitted from abilities and return types.
Here, ε is a distinguished effect variable, the implicit effect variable that is fresh for every type signature in a program. This syntactic sugar ensures that we need never write the implicit effect variable ε in a Frank program. In adaptors, we write I as syntactic sugar for the common case of mask, i.e. I(s a → s). Type binders Z may be value binders X or effect binders [E]; polytypes may be polymorphic in both value and effect types. Though we avoid effect variables in source code, we are entirely explicit about them in the abstract syntax and type system.
Data types and effect interfaces are defined globally. A definition for data type D(Z) is a collection of data constructor signatures of the form k : A, where A may depend on Z. Each data constructor belongs to a single data type. A definition for effect interface I(Z) consists of a collection of command signatures of the form c : ∀Z .A →B, denoting that c is polymorphic in type variables Z , takes arguments of types A, and returns a value of type B.
The types A and B may depend on Z and Z . Each command belongs to a single interface.
We have two kinds of environments. binds monomorphic and polymorphic term variables to value types. binds interface pattern variables to interface instances.

Effect Parameters with an Invisible Effect Variable.
The body of the definition of a data type or effect interface may use an implicit effect variable. Correspondingly, in this case an implicit parameter is added as the last parameter of the data type definition. We give an example in Section 5.

Abilities and Adjustments.
An ability is a collection of interface instances initiated either with the empty ability ∅ (yielding a closed ability) or an effect variable E (yielding an open ability). Order is important, as duplicates are permitted, in which case the rightmost interface instance is the one corresponding to the active handler. Closed abilities are not normally required, but they can be used to enforce purity, for instance. In ASCII source code we write ∅ as 0.
An adjustment modifies an ability . We introduce a system of inference rules to describe the action of an adjustment = | on an ability . The judgements take the general form: Input Contexts Transformer Output Contexts where "Transformer" produces one or more output contexts (separated by ";") by acting on one or more input contexts. Such a system of rules is reminiscent of algorithmic presentations of polymorphic type systems (Gundry et al., 2010;Dunfield & Krishnaswami, 2013) which account for and track the knowledge gained-solutions to unification constraintsduring the process of typechecking. We utilise the same machinery to account for information increase with respect to the effects offered by the ambient ability: starting from effects globally available to those locally permitted. Figure 3 presents the judgement for computing the action of an adjustment on an ability along with five further auxiliary judgements: • states that the action of adjustment on ability yields ability ; • states that the action of extension on ability yields ability ; • states that the action of adaptor on ability yields ability ; • I(S → S ) states that the action of adaptor component I(S → S ) on ability yields ability ; • S : I ; states that adaptor pattern S at interface I yields residual ability and binds instance environment ; and • S : I states that in instance environment adaptor pattern S at interface I yields extension .
The action of an adjustment on an ability (A-ADJ) is obtained by first applying the adaptor to the ability and then applying the extension to the result.
The action of an extension on an ability (the A-EXT-rules) is to add each effect instance to the ability. The action of an adaptor on an ability (the A-ADAPT-rules) transforms the input ability by applying its individual adaptor components in sequence. The action of an adaptor component (A-ADAPT-COM) first computes the residual ability and instance environment for the input pattern, then uses the instance environment along with the output pattern to compute an extension, which is finally applied to the residual ability. The pattern matching rules (I-PAT-) yield a residual ability and an instance environment by matching on variables in S. The instantiation rules (I-INST-) yield an extension by instantiating the patterns in S using the instance environment .
Whereas abilities may contain duplicate effect instances, both the adaptor and extension components of an adjustment must not. For convenience, we sometimes use functional notation for the action of an adjustment, extension, or adaptor on an ability. For instance, we write: ( ) for when . We define the operation @ I to be the extension which contains all instances of I occurring in : We let | | denote the length of the extension . We define an indexing operation on extensions. Given an extension and 0 ≤ i < | |, define (i) as follows: This operation is only defined for 0 ≤ i < | | so for brevity, whenever we write (i) we implicitly assume that i satisfies this inequality.
Terms. Frank follows a bidirectional typing discipline (Pierce & Turner, 2000). Thus, terms ( Figure 2) are subdivided into uses (ranged over by m) whose types are inferred, and constructions (ranged over by n) which are checked against a type. Uses comprise monomorphic variables x, polymorphic variable instantiations f R, applications m n, and type ascriptions ↑(n : A). Constructions comprise uses ↓m, data constructor instances k n, command invocations c R n, suspended computations {e}, polymorphic let (let f : P = n in n ), mutual recursion (letrec f : P = e in n), and adaptors n. For clarity in the formalism, we explicitly mark the injections of a use into a construction (↓m) and a construction into a use (↑(n : A)); in actual Frank code, we always omit ↓ and ↑. We write ! for the empty sequence of constructions.
A computation is defined by a sequence of pattern matching clauses r → n. Each pattern matching clause takes a sequence of computation patterns r. A computation pattern is either a standard value pattern p, a request pattern c p → z , which matches command c if its arguments match p (which are in turn bound) and binds the continuation to z, or a catch-all pattern x , which matches any value or handled command, binding it to x. A value pattern is either a data constructor pattern k p or a variable pattern x.

22
L. Convent et al. Fig. 3. Action of an adjustment on an ability and auxiliary judgements.

Example.
To illustrate how source programs may be represented as abstract syntax, we give the abstract syntax for an example involving the map, state, and index operators from Section 2. The map function and state handler are recursive, so are defined using letrec, whereas the index function is not recursive so is defined with let. The type signatures are adorned with explicit universal quantifiers and braces to denote that they each define suspended computations. Pattern matching by equations is represented by explicit pattern matching in the standard way. Each wildcard pattern is represented with a fresh variable.
Frank Design. Inspiration for the Frank is drawn from Levy's CBPV calculus (Levy, 2004). Indeed, as in CBPV, we separate value and computation types. However, the similarities really end there. Frank has no equivalent to the CBPV "producer" (F) types, and more significantly, Frank does not distinguish terms by whether they are a computation or a value. Indeed, CBPV's eponymous rule for always applying a function to a value does not carry over to Frank which instead generalises functional abstraction to incorporate effect handling in situ. Our design decision is motivated by a desire to reduce boilerplate in source programs and contrasts with other languages incorporating effect handling (Pretnar, 2009;Bauer & Pretnar, 2014;Ahman, 2017), where a separate handling construct is introduced. Hence, Frank combines the features of CBPV, call-by-value (a la ML), and effect typing to smoothly integrate well-typed effectful code within the functional paradigm.

Typing rules
Before defining the typing judgements for Frank, we first define the well-formedness judgement in Figure 4: X states that X is well-formed in kind environment . (The meta variable X ranges over the various syntactic categories defined in Figure 1 as well as existential type environments ∃ . , which will be used for typing handlers 24 L. Convent et al. for polymorphic commands (the T-COMP rule in Figure 5 and the P-COMMAND rule in Figure 6).) The well-formedness rules are routine, following the structure of X . The wellformedness judgement is to the usual intuitionistic structural rules (weakening, contraction, and exchange).
The term typing rules for Frank are given in Figure 5. Each judgement is defined with respect to kind environment and is implicitly subject to the condition that each of its subcomponents must be well-formed with respect to : • ; [ ]--m ⇒ A states that in type environment with ambient ability , we can infer that use m has type A; • ; [ ]--n : A states that in type environment with ambient ability , construction n has type A; • ; e : C states that in type environment , computation e has type C; • r : T --[ ] ∃ . states that computation pattern r of argument type T with ambient ability binds existentially quantified type environment ∃ . ; • p : A states that value pattern p of type A binds type environment .
The first three judgements are subject to the usual intuitionistic structural rules (weakening, contraction, and exchange). The T-VAR rule infers the type of a monomorphic variable x by looking it up in the environment; T-POLYVAR does the same for a polymorphic variable instantiation f R, but also substitutes R for Z in its type. The T-APP rule infers the type of an application m n under ambient ability . First it infers the type of m of the form { A → [ ]B}. Then it checks that = and that each argument n i matches the inferred type in the ambient ability modified by adjustment i . If these checks succeed, then the inferred type for the application is B. The T-ASCRIBE rule ascribes a type to a construction allowing it to be treated as a use.
The T-SWITCH rule allows us to treat a use as a construction. The checking rules for data types (T-DATA), suspended computations (T-THUNK), polymorphic let (T-LET), mutual recursion (T-LETREC), and adaptors (T-ADAPT) recursively check the sub-terms. The T-COMMAND rule looks up the type of the command c in the ambient ability.
The T-APP and T-ADAPT rules have side conditions on the computed abilities to ensure that they are well-defined (recall that the action of an adaptor/adjustment on an ability is a partial operation).
A computation of type T → G is built by composing pattern matching clauses of the form r → n (T-COMP), where r is a sequence of computation patterns whose variables are bound in n. The side condition in the T-COMP rule requires that the patterns in the clauses cover all possible values inhabiting the argument types. While we do not discuss coverage checking any further here, Lindley et al. (2017) have presented an elaboration algorithm for Frank which checks coverage.
The typing rules for pattern matching are presented in Figure 6. Value patterns match variables (P-VAR) and data type constructor applications (P-DATA). Value patterns can be typed as computation patterns (P-VALUE). Catch-all patterns suspend the computation (P-CATCHALL). For both value patterns and catch-all patterns, we apply the adjustment to the ambient ability and we omit the existential in the conclusion as is empty. A command pattern c p → z may be checked at type B with ambient ability (P-COMMAND). The command c must appear in the extension of . The continuation is a plain function so its argument type has the identity adjustment. The continuation's return type has the ambient ability with applied. The existential quantifier ensures that the type variables Z cannot escape.
Instantiating ε. In an earlier version of Frank, the POLYVAR rule was restricted to always instantiate the implicit effect variable ε with the ambient ability . Correspondingly, data types were restricted to being parameterised by at most one effect variable, namely ε. The language resulting from these restrictions has the pleasant property that effect variables need never be written at all. However, we now feel that the restrictions are artificial, and having multiple effect variables may be useful. Given that the APP rule already checks that abilities match up exactly where needed, relaxing the POLYVAR rule does no harm, and now we can support data types parameterised by multiple effect variables.

Definition 1 (Simultaneous Substitution). For type environments and , let θ : denote the simultaneous substitution θ of variables in environment by use values which are well-typed in environment . More formally, we define the judgement
[ ]--θ : via the following rule: [ ]--θ : Since the type of a use value is independent of the ambient ability, we write simultaneous substitutions as θ : for short. We write [u/x] for the unary special case substituting a use value u for a variable x at some type A, corresponding to , x : A for any . We write [u/x] for the simultaneous substitution, substituting each u i for each x i .

Abusing notation slightly, we write [↑(w : ∀Z.A)/f ] for the substitution of ↑(w : A[R/Z]) for f R with a corresponding generalisation for simultaneous substitution.
Our typing rules are closed under standard weakening so we may extend the codomain of any simultaneous substitution with a free (poly) variable.
Lemma 3 (Substitution Preserves Typing). Given , and θ : : Runtime Syntax. The operational semantics relies on the runtime syntax defined in Figure 7. Both uses and constructions are augmented with a special form E [c R w] of frozen commands described below. We distinguish between use values and construction values. Moreover, we also distinguish those construction values which are not uses, as these are the ones that can appear in type ascriptions in use values. We define a special class of normal forms, which are either construction values w or frozen commands. Normal forms can be regarded as generalised values that may be passed to operators. Evaluation contexts are defined as sequences of evaluation frames. Evaluation frames are mostly unsurprising. The interesting case is u (t, [ ], n) which enables evaluation to proceed on arguments left-to-right until each argument becomes a normal form. Following the separation between uses and constructions, evaluation contexts also ramify as such, leading to four distinct classes; for each possible combination of hole and body (see the R-LIFT-* rules in Figure 9). However, we do not explicitly distinguish the four kinds of evaluation contexts in the syntax but instead rely on the type system to do so.
Freezing Commands. In order to handle a command, we need to capture its delimited continuation, that is, the largest enclosing evaluation context that does not handle it. A frozen command E [c R w] is a command application c R w plugged inside an evaluation context E that does not handle c. Frozen commands only occur at runtime (they cannot be written in programs). The typing rules for frozen commands are given in Figure 8. Evaluation contexts can either be uses or constructions; hence, we need two typing rules. The side conditions depend on an auxiliary predicate E handles c, which is true if c is handled by E .

28
L. Convent et al. Evaluation Context Adaptor Functions. In order to determine whether an evaluation context handles a given command, we first compute an adaptor function which describes how the evaluation context transforms effects. In order to account for extensions, we write ⊥ (a distinguished pattern variable) to denote a concrete instantiation of an interface. For instance s → s ⊥ is an adaptor function that denotes the handling of an interface, and s a → s ⊥ is an adaptor function that denotes erasing one copy of an interface from the ambient ability and then handling another copy, and is obtained as the composition: Composition is defined by substitution: such that |a 2 | = |b 1 |. We can always ensure that the arities match up using the equation: s a a → s a v) for some fresh a.
The adaptor function of an evaluation context is defined by composing together the adaptor functions of its constituent evaluation frames.
The adaptor function for an evaluation frame is the identity (s → s) for all frames except argument frames and adaptor frames for which it is defined as follows: The adaptor function af(c, ) for a command c in an extension is as follows: The adaptor function af(c, ) for a command c in an adaptor is as follows: The predicate E handles c holds iff af(c, E ) has the form S → S ⊥.
We also define another predicate E poisedfor c which captures the intuition that a c handler placed around E [c u] will handle c. The predicate E poisedfor c holds iff af(c, E ) is equivalent to an adaptor function of the form S a → S a or s → s. So, in particular, the empty evaluation context is always waiting for the nearest dynamically enclosing handler to handle any command.
The adaptor function computes a transformation on the ambient ability. That is, the direction of information flow is inwards, computing the local ability by transformation on the effects offered globally. Of course, we can reverse the direction of information flow and compute the ambient ability by retracing our steps through the evaluation frames. This second viewpoint captures the notion of effect handling and forwarding which we formalise via a rewiring operation on the output of adaptor functions. Given a syntactic phrase class P such that af(c, P) = (s a → s b) and natural number i, we define the operation af(c, P)(i) or, equivalently (s a → s b)(i), as follows: Recall, the list of patterns are snoc lists and indexing is right-to-left. So, for example, index 0 corresponds to the rightmost instance in the list. The rewiring operation is a crucial component for proving the Frank type system is sound. Indeed, we must establish a connection between rewiring and the static transformations on abilities performed by the type system. Together, they encompass the two viewpoints outlined above. The statics describe how the ambient ability offered at the top-level is transformed and subsequently offered locally. The dynamics (rewiring) describe how to satisfy a local request by forwarding the request upwards through evaluation frames, arriving at the top-level if and only if the request can be satisfied by the ability. Later, we define a relationship between normal forms and abilities (Definition 6) which formally characterises this connection between the static and dynamic viewpoints. The semantics is given in Figure 9. We define four reduction relations: top-level use reduction (m u m ), top-level construction reduction (n c n ), full use reduction (m −→ u m ), and full construction reduction (n −→ c n ).
The R-HANDLE (Figure 9) rule depends on two auxiliary judgements ( Figure 10). The judgement r : T ← t --[ ] θ states that computation pattern r of argument type T at ability matches normal form t yielding substitution θ . The judgement p : A ← w --[ ] θ states that value pattern p of type A at ability matches construction value w yielding substitution θ . The R-HANDLE rule reduces an application according to a first-match semantics. The R-ASCRIBE-USE and R-ASCRIBE-CONS rules remove redundant type ascriptions from use and construction values, respectively. The R-LET and R-LETREC rules are standard. The R-ADAPT rule removes an adaptor once the construction it contains is a value. The R-FREEZE-COMM rule freezes a command in the identity evaluation context. The R-FREEZE-FRAME-rules extend the evaluation context of a frozen command by one frame. The R-LIFT-UU, R-LIFT-UC, R-LIFT-CU, and R-LIFT-CC rules lift the top-level reduction relations to full reduction relations.
Theorem 5 (Subject Reduction). Programs are constructions. If a program stops reducing, then it must be a normal form, that is, either a value or a frozen command (and the latter only if the ambient ability is non-empty). We formally specify normal forms in Definition 6, complete with necessary side conditions that marry the static semantics enforced by the Frank type system and the dynamic semantics enforced by the operational semantics. Crucially, the definition connects the action of a construction on the ambient ability, which is computed statically by the typing rules, to the dynamic rewiring performed by the adaptor function. The side conditions tell us that any command which is forwarded all the way to the top-level was in fact offered by the ambient ability.
Definition 6. We say that normal form t respects if it is either a value w or a frozen command E [c R w] such that c ∈ I and af(c, E )(0) = i < | @I| and c R ∈ ( @I)(i).
Theorem 7 (Type Soundness). In particular, if = ∅, then either the term is a value w or the term can reduce by one step.
Proof. By simultaneous induction on the typing derivations ·; · [ ]--m ⇒ A and ·; · [ ]-n : A using subject reduction (Theorem 5). Additionally, the proof relies on a number of auxiliary lemmas connecting the static typing rules for the action of an adjustment on an ability (Figure 3), and the rewiring operation af(−, −)(−) on adaptor functions. We present the full details of the proof in the Appendix.

Computations as data
So far, our example data types have been entirely first order, but our type system admits data types which abstract over abilities exactly to facilitate the storage of suspended computations in a helpfully parameterised way. When might we want to do that? Let us develop an example, motivated by Shivers and Turon's treatment of modular rollback in parsing (Shivers & Turon, 2011). Consider a high-level interface to an input stream of characters with one-step lookahead. A parser may peek at the next input character without removing it, and accept that character once its role is clear.
interface LookAhead = peek : Char | accept : Unit We might seek to implement LookAhead on top of regular Console input, specified thus: interface Console = inch : Char | ouch : Char -> Unit where an input of '\b' indicates that the backspace key has been struck. The appropriate behaviour on receipt of backspace is to unwind the parsing process to the point where the previous character was first used, then await an alternative character. To achieve that unwinding, we need to keep a log, documenting what the parser was doing when the console actions happened.
Note that although Log is not explicitly parameterised by an ability, it must be implicitly parameterised as it stores implicitly effect-polymorphic suspended computations. The above definition is shorthand for the following: As discussed in Section 4, the general rule is that if the body of a data type (or interface) definition includes the implicit effect variable, then the last parameter of the definition is also the implicit effect variable. Initially (start) a log contains a parser computation. When a character is input (inched) the log is augmented with the continuation of the parser, which depends on the character read, allowing the continuation to be replayed if the input character changes. When a character is output (ouched) there is no need to store the continuation as the return type of an output is Unit and hence cannot affect the behaviour of the parser. Modular rollback can now be implemented as a handler informed by a log and a one character buffer.

data Buffer = empty | hold Char
The parser process being handled should also be free to reject its input by aborting, at which point the handler should reject the character which caused the rejection.
input : { Log X [ LookAhead , Abort ] -> Buffer -> < Console | LookAhead , Abort >X -> [ Console ]X} input _ _ x = x input l ( hold c) < peek -> k > = input l ( hold c) (k c) input l ( hold c) < accept -> k > = ouch c; input ( ouched l) empty (k unit ) input l empty < accept -> k > = input l empty (k unit ) input l empty < peek -> k > = case inch ! { '\b ' -> rollback l | c -> input ( inched l k) ( hold c) (k c) } input l _ < abort -> k > = rollback l Note the use of an adaptor adjustment in the third argument to remove the Console effect from the ambient ability. In doing so, we can ascribe a more precise ability to the Log type, prohibiting the storage of continuations which perform Console commands. Correspondingly, it is clear that the parser's continuations may be stored, and under which conditions those stored continuations can be invoked, when we rollback. To undo an ouch, we send a backspace, a blank, and another backspace, erasing the character. To undo the inch caused by a "first peek", we empty the buffer and reinvoke the old continuation after a new peek.
Here is a basic parser that accepts a sequence of zeros terminated by a space, returning the total length of the sequence on success, and aborting on any other input. In order to implement actual console input and output, the Console interface is handled specially at the top-level using a built-in handler that interprets inch and ouch as actual character input and output. The entry point for a Frank program is a nullary main operator.

main : {[ Console ] Int } main ! = parse ( zeros 0)
The ability of main is the external ability of the whole program. We can use it to configure the runtime to the execution context: is it a terminal? is it a phone? is it a browser? What will the user let us do? Currently, Frank supports a limited range of built-in top-level interfaces, but one can imagine adding many more and in particular connecting them to external APIs. We have revisited an example presented by Lindley et al. (2017) but have been able to give a more constrained type to the parser process, preventing it from accessing Console commands by using an adaptor in the signature for input to mask this effect. No changes to the implementation were necessary.

Concurrency in Frank
In this section, we implement concurrent actors with mailboxes in Frank. This application serves also as an example for the usefulness of adaptors, as a comparison to an earlier implementation of this system shows (Convent, 2017). We present the actor system in stages, by composing together several handlers. Effect Interfaces. Let us begin with the high-level actor effect interface Actor M for an actor computation whose mailbox stores messages of type M. The Pid M data type represents a process ID for a process with a mailbox of type M. We define it later in this section. The Actor M interface supports four commands: spawn runs the supplied thunk as a new process for which a fresh process ID is generated and returned, self returns the process ID of the current process, send places a message in the mailbox of the specified process, and recv removes and returns a message from the mailbox of the current process. As a first step, we define a cooperative concurrency effect interface Co.
interface Co = fork : {[ Co ] Unit } -> Unit | yield : Unit It supports two commands: fork runs the supplied thunk as a new process and yield allows control to switch to another process. A suspended computation passed to fork may itself fork and yield, hence the interface is recursive. In order to implement cooperative concurrency, we store suspended processes in a queue via the effect interface Queue S for a queue with elements of type S.
interface Queue S = enqueue : S -> Unit | dequeue : Maybe S It supports two commands: enqueue pushes a value onto the queue and dequeue pops a value x from the queue and returns just x if the queue is non-empty and returns nothing if the queue is empty. Now we provide implementations for each of our three interfaces in turn from low-level to high-level.
Queues. One might imagine various different implementations for queues; here, we focus on one based on a zipper structure (Huet, 1997).

data ZipQ S = zipq ( List S) ( List S)
A queue is represented as a pair of lists: the first is the front of the queue, supporting amortised constant time popping from the front of the queue; the second is the back of the queue in reverse, supporting constant time pushing to the back of the queue.
An empty queue is a pair of empty lists.
Schedulers. Using our queue interface, we can define schedulers for concurrent processes. The processes we store in the queue manipulate the queue by themselves, so we represent them through a recursive data type. Two canonical schedulers are breadth-first (forked processes are deferred) and depth-first (forked processes are run eagerly). We use the former for our actor implementation, but we could easily swap in an alternative if desired.
runBF : {<Co > Unit -> [ Queue Proc ] Unit } runBF < yield -> k > = enqProc { runBF (k unit ) }; wakeProc ! runBF < fork p -> k > = enqProc { runBF (< Queue > p !) }; runBF (k unit ) runBF unit = wakeProc ! An adaptor is required in order to allow a forked process to have the right type. Without the adaptor, the argument type to the scheduler would be polluted and have to become <Co [ Queue Proc]>. Allowing processes to do their own low-level manipulation of the process queue breaks abstraction.

Actors.
We now define actors on top of cooperative concurrency, queues, and references. Let us first define a data type for process IDs.
data Pid X = pid ( Ref ( ZipQ X)) A process ID stores its associated mailbox as a mutable reference to a queue. The core of the actor implementation is given by the runActor handler.
To test the actor implementation, we implement a classic example that spawns a chain of processes and passes a message along the chain. each spawned process prints out a dot as the message is passed along and the message received by the top-level process ("do be do be do") is printed out after having been transmitted all the way along the chain. The outer adaptor ensures that the two instances of RefState are mapped to the same built-in RefState effect.

Implementation
The third author has been plotting Frank since at least 2007 (McBride, 2007). In 2012, he implemented a prototype for a previous version of Frank (McBride, 2012). Since then the design has evolved. A significant change is the introduction of operators that handle multiple computations simultaneously. More importantly, a number of flaws in the original design have been ironed out as a result of formalising the type system and semantics. We have implemented a prototype of Frank (Convent et al., 2020) in Haskell following the design described in the current paper. In order to rapidly build a prototype, we consciously decided to take advantage of a number of existing technologies. The current prototype takes advantage of the indentation sensitive parsing framework of Adams & Agacan (2014), the "type-inference-in-context" technique of Gundry et al. (2010), and the existing implementation of shonky (McBride, 2016), which we have modified slightly to support features like adaptors and references.
Much like Haskell, in order to aid readability, the concrete syntax of Frank is indentation sensitive (though we do not explicitly spell out the details in the paper). In order to implement indentation sensitivity, Adams & Agacan (2014) introduce an extension to parsing frameworks based on parsing expression grammars. Such grammars provide a formal basis for the Parsec (Leijen & Martini, 2015) and Trifecta (Kmett, 2015) parser combinator Haskell libraries. In contrast to the ad hoc methods typically employed by many indentation sensitive languages (including Haskell and Idris Brady, 2013a), Adams & Agacan's extension has a formal semantics. Frank's parser is written using Trifecta with the indentation sensitive extension, which greatly simplifies the handling of indentation by separating it as much as possible from the parsing process.
For bidirectional typechecking, our prototype uses Gundry et al.'s "type-inference-incontext" technique (Gundry et al., 2010) for implementing type inference and unification (Gundry's thesis Gundry, 2013 contains a more detailed and up-to-date account). The key insight is to keep track in the context not just the term variables but also the unification variables. The context enforces an invariant that later bindings may only depend on earlier bindings. The technique has been shown to scale to the dependently typed setting (Gundry, 2013).
For convenience, the implementation of Frank provides interface aliases which name the composition of several interfaces. For instance, we can compose interfaces for sending and receiving to obtain a state interface.
interface Send X = send : X -> Unit interface Receive X = receive : X interface State X = [ Send X , Receive X] The back end of Frank is implemented by first translating to shonky (McBride, 2016), which amounts to an untyped version of Frank. The shonky implementation executes code directly through an abstract machine much like that of Hillerström et al. (2020). The evaluation contexts in the formalism correspond to stack frames in the abstract machine.

Adaptor variations and extensions
Adaptors are one particular solution to the effect pollution problem and we have detailed one particular design for adaptors. In this section, we discuss variations and extensions of adaptors.
Adaptors Before Extensions. As spelled out in Section 4, the action of an adjustment on an ability first applies the adaptor and then applies the extension.
( | )( ) = ( ( )) If we were to switch the order ( | )( ) = ( ( )) then we could obtain a slightly more expressive system in which one could handle an instance of an interface other than the rightmost one. For example, an argument type <Abort(s a b -> s b a)|Abort>Int would handle the second instance of Abort rather than the first as it does now. A disadvantage of switching the order is that common cases such as mask become a little more complicated.

Deep Extensions.
We have implemented an extension to adaptors in Frank whereby multiple instances of the same effect interface can be handled by the same effect handler. We distinguish the instances by indexing by position in command patterns. For instance, the following handler is able to handle Reader Bool and Reader Int effects at the same time.
Extensible Adaptors. Another design that may be worth investigating would be to roll extensions into adaptors. For instance, Reader(s -> s (Int)) would indicate the adjustment |Reader Int and Reader(s a -> s (Int) a) an adjustment that handles the second instance of Reader.
Adaptive Abilities. A non-uniformity in our current design is that both adaptors and extensions appear in adjustments yet only extensions appear in abilities. One obtains a more expressive system by changing the syntax of abilities to: The adaptor in an ability applies after the effect variable has been instantiated. This requires some care because there is no guarantee that an adaptor pattern will match. A solution is to change the semantics of pattern matching such that variables are always bound to (possibly empty) lists of instances and the body of an adaptor component concatenates all of the lists together. For instance, suppose we apply the adaptor Reader(s a b -> s b) to the ability 0|Reader Int, then b is bound to the singleton list containing the one instance of Reader Int and a and s are both bound to the empty list. With adaptive abilities, one can define an adaptor that disables all instances of an interface. For instance, we could disable all instances of Console with the adaptor: Console(s ->).
Inference. One might hope to automatically infer which inline adaptors need to be inserted in order to make a program typecheck. This is possible up to a point, but clearly not in general as there cannot be a unique or most general solution. For instance, if we need to coerce a computation with ability Reader Int to one with ability Reader Int, Reader Int, then we must choose between two possible adaptors: Reader(s a -> s) and Reader(s a b -> s b).

Related work
We have discussed much of the related work throughout the paper. Here, we briefly mention some other related work.

Algebraic Theory of Effects and Handlers. Effect handlers were originally built on
Plotkin and Power's algebraic theory of effects (Plotkin & Power, 2001a,b, 2002, 2003. In Plotkin and Power's setting, algebraic effects are accompanied by an equational theory governing the behaviour of the operations. In all of the implementations we have discussed (Eff, Helium, Koka, Links), and in Frank, the equational theory is taken to be the free theory, in which there are no equations. Moreover, none of these implementations guarantees that effect handlers are algebraic.
Efficient Effect Handler Implementations. A natural implementation for handlers is to use free monads (Kammar et al., 2013). Swierstra (2008) illustrates how to write effectful programs with free monads in Haskell, taking advantage of type classes to provide a certain amount of modularity. However, using free monads directly can be quite inefficient (Kammar et al., 2013). Wu & Schrijvers (2015) show how to obtain a particularly efficient implementation of deep handlers taking advantage of fusion. Their work explains how Kammar et al. (2013) implement efficient handler code in Haskell. Kiselyov & Ishii (2015) optimise their shallow effect handlers implementation, which is based on free monads, by taking advantage of an efficient representation of sequences of monadic operations (van der Ploeg & Kiselyov, 2014). The experimental multicore extension to OCaml (Dolan et al., 2015) extends OCaml with effect handlers motivated by a desire to abstract over scheduling strategies. It does not include an effect system. It does provide an efficient implementation by optimising for the common case in which continuations are invoked at most once (the typical case for a scheduler). The implementation uses the stack to represent continuations and as the continuation is used at most once there is no need to copy the stack. Koka (Leijen, 2017) takes advantage of a selective CPS translation to improve the efficiency of generated JavaScript code.

Layered Monads and Monadic Reflection.
Filinski's work on monadic reflection (Filinski, 2010) and layered monads (Filinski, 1999) is closely related to effect handlers. Monadic reflection supports a similar style of composing effects. The key difference is that monadic reflection interprets monadic computations in terms of other monadic computations, rather than abstracting over and interpreting operations. Swamy et al. (2011) add support for monads in ML, providing direct-style effectful programming for a strict language. Unlike Frank, their system is based on monad transformers rather than effect handlers. Schrijvers et al. (2019) compare the expressiveness of effect handlers and monad transformers in the context of Haskell. Forster et al. (2017Forster et al. ( , 2019 compare effect handlers with monadic reflection and delimited control in a more abstract untyped setting; Piróg et al. (2019) extend this approach in order to compare effect handlers with delimited control in the presence of types, making essential use of polymorphism.
Variations and Applications. Lindley (2014) investigates an adaptation of effect handlers to more restrictive forms of computation based on idioms (McBride & Paterson, 2008) and arrows (Hughes, 2004). Wu et al. (2014) study scoped effect handlers. They attempt to tackle the problem of how to modularly weave an effect handler through a computation whose commands may themselves be parameterised by other computations. Kiselyov & Ishii (2015) provide solutions to particular instances of this problem. Schrijvers et al. (2014) apply effect handlers to logic programming. Effect Instances. Brady (2013b) provides a lightweight syntax for statically supporting multiple instances of the same effect. Early versions of the Eff programming language (Bauer & Pretnar, 2014 include a facility to dynamically generate fresh effect instances offering an alternative way to encapsulate effects. We could consider adding a fresh effect construct to Frank. In order to do so, we would need to extend the effect type system to provide interface variables (along the lines of a region type system) and this may admit programming patterns that are awkward or impossible with only adaptors. On the other hand, fresh effect generation is not expressive enough to define mask, so we would probably still want to keep adaptors. In contrast, for row-based effect type system that do not allow effect shadowing, such as the one employed by Links (Hillerström & Lindley, 2016;Hillerström et al., 2020), a fresh effect construct would suffice, as there is no need for mask anyway. Biernacki et al. (2020) propose a novel form of effect instances using lexically scoped effect handlers. Lexically scoped effect handlers are not quite as expressive as the usual dynamically scoped effect handlers, but it remains to be seen whether the restricted expressive power is a significant impediment in practice. In his PhD thesis, Bram Geron studies the foundations for algebraic lexically scoped effect handlers through a construct he calls defined algebraic operations (Geron, 2019).

Effect Instances and Lexically Scoped Effect Handlers.
Inject, Lift, and Mask. Inject, lift, and mask are different names for the same construct. Leijen's Koka language (Leijen, 2014) has a similar row-based effect type system to Frank in which effects can shadow one another. When Koka was originally conceived it had no effect handlers. Nevertheless, Leijen discusses inject (Leijen, 2014) as a way to address a particular instance of the effect pollution problem for hard-coded exceptions. Koka now supports effect handlers (Leijen, 2017) and a generalised inject for arbitrary effects (Leijen, 2018). Biernacki et al. (2018) introduce an effect handler calculus λ H/L , inspired by Koka. They construct a logical relation which they use for reasoning about effect handler programs. Inspired by a desire for greater parametricity, they incorporate lift. They also observe that they can macro-express a generalised lift operator and a swap operator using plain lift and effect handlers. Extending λ H/L , Biernacki et al. (2019) introduce a calculus λ HEL combining existential types, effect instances with a region-like type system, and a generalisation of lift similar to adaptors that they call coercions. Unlike adaptors which are expressed as lambda expressions, coercions are constructed from four basic combinators: lift, swap, cons, and compose. These coercions are not quite as expressive as adaptors as they do not support duplicating an effect (as in our state actors example), though one can work around this limitation using a handler.
Modules. Multicore OCaml (Dolan et al., 2015) supports effect handlers without effect types. The OCaml module system provides a mechanism for simulating effect instances. Biernacki et al.'s programming language Helium (Biernacki et al., 2019) also incorporates a module system, which is elaborated into λ HEL using existential types.

Negative Adjustments.
In an earlier version of this paper  we proposed negative adjustments as a means for masking effects from operator arguments; these correspond exactly to the standard mask adaptor adjustment of our current design.
Effect Tunneling. Zhang & Myers (2019) present an alternative approach to ensure handlers encapsulate effects. Their key idea is to extend types with handler variables and handler polymorphism. The variables act as labels on effects in the ambient context and determine which handlers interpret which effects. Handler polymorphism is resolved by substituting the nearest lexically enclosing handler for the handler variable. A capability region system ensures that computations do not escape their handlers. Following Biernacki et al. (2018), they develop a sound logical relations model and prove their system satisfies an abstraction theorem. While Zhang and Myers do not present an implementation, they claim that the programmer need not deal with handler variables in practice. Rather, they outline a desugaring and rewriting pass which inserts the requisite variable bindings and handler instantiations.

Future work
We have further progress to make on many fronts, theoretical and practical.
Multihandlers in other Calculi. Multihandlers appear to share some features of other expressive programming abstractions such as multiple dispatch and join patterns (Fournet & Gonthier, 1996), and it would be interesting to better understand how multihandlers relate to these abstractions.

Verbs versus Nouns.
Our rigid choice that names stand for values means that nullary operators need ! to be invoked. They tend to be much more frequently found in the doing than the being, so it might be prettier to let a name like jump stand for the "intransitive verb", and write {jump} for the "noun". Similarly, there is considerable scope for supporting conveniences such as giving functional computations by partial application whenever it is unambiguous.

Data as Computations.
Frank currently provides both interfaces and data types. However, given an empty type 0, we can simulate lazy coinductive data types using interfaces (and data constructors using commands). Data constructors can be seen as exceptions, that is, commands that do not return a value. For instance, we can encode lazy possibly infinite lists as follows: Note that the pattern matching clauses are complete because the return type is uninhabited.
Given that computations denote free monads (i.e. trees) and data types also denote trees, it is hardly surprising that there is a correspondence here. Indeed Atkey's effect handlers account of typechecking and elaboration (Atkey, 2015) makes effective use of this correspondence. We would like to study abstractions for more seamlessly translating back and forth between computations and data.
Failure and Choice in Pattern Matching. We could extend Frank to realise incomplete or ambiguous patterns as effects. Pattern matching can fail (if the patterns are incomplete) or it can succeed in multiple ways (if the patterns are redundant). Thus, in general pattern, matching yields a searchable solution space. We can mediate failure and choice as effects, separating what it is to be a solution from the strategy used to find one. Concretely, we envisage the programmer writing custom failure and choice handlers for navigating the search space. Wu et al. (2014) have shown the modularity and flexibility of effect handlers in managing backtracking computations: the design challenge is to deploy that power in the pattern language as well as in the expression language.
Controlled Snooping. We might consider allowing handlers to trap some or even all commands generically, just as long as their argument types make this possibility clear. Secret interception of commands remains anathema.

Indexed Interfaces.
Often, an interaction with the environment has some sort of state, affecting which commands are appropriate, e.g. reading from files only if they open. Indeed, it is important to model the extent to which the environment determines the state after a command. McBride (2011) observes that indexing input and output types over the state effectively lets us specify interfaces in a proof-relevant Hoare logic. Hancock & Hyvernat (2006) have explored the compositionality of indexed "interaction structures", showing that it is possible to model both sharing and independence of state between interfaces.
Session Types as Interface Indices. Our pipe is a simple implementation of processes communicating according to a rather unsubtle protocol, with an inevitable but realistic "broken pipe" failure mode. We should surely aim for more sophisticated protocols and tighter compliance. The interface for interaction on a channel should be indexed over session state, ensuring that the requests arriving at a coordinating multihandler match exactly.
Substructural Typing for Honesty with Efficiency. Using Abort, we know that the failed computation will not resume under any circumstances, so it is operationally wasteful to construct the continuation. Meanwhile, for State, it is usual for the handler to invoke the continuation exactly once, meaning that there is no need to allocate space for the continuation in the heap. Moreover, if we want to make promises about the eventual execution of operations, we may need to insist that handlers do invoke continuations sooner or later, and if we want communicating systems to follow a protocol, then they should not be free to drop or resend messages. Linear, affine, and relevant type systems offer tools to manage uses more tightly: we might profitably apply them to continuations and the data structures in which they are stored.
Modules and Type Classes. Frank's effect interfaces provide a form of modularity and abstraction, tailored to effectful programming in direct style. It seems highly desirable to establish the formal status of interfaces with respect to other ways to deliver modularity, such as ML modules (MacQueen, 1984) and Haskell type classes (Wadler & Blott, 1989).
Totality, Productivity, and Continuity. At heart, Frank is a language for incremental transformation of computation (command-response) trees whose node shapes are specified by interfaces, but in the "background", while keeping the values communicated in the foreground. Disciplines for total programming over treelike data, as foreground values, are the staple of modern dependently typed programming languages, with the state of the art continuing to advance (Abel & Pientka, 2013). The separation of client-like inductive structures and server-like coinductive structures is essential to avoid deadlock (e.g. a server hanging) and livelock (e.g. a client constantly interacting but failing to return a value). Moreover, local continuity conditions quantifying the relationship between consumption and production (e.g. spacer consuming one input to produce two outputs) play a key role in ensuring global termination or productivity. Guarded recursion seems a promising way to capture these more subtle requirements (Atkey & McBride, 2013).
Given that we have the means to negotiate purity locally while still programming in direct style, it would seem a missed opportunity to start from anything other than a not just pure but total base. To do so, we need to refine our notion of "ability" with a continuity discipline and check that programs obey it, deploying the same techniques total languages use on foreground data for the background computation trees. McBride has shown that general recursion programming fits neatly in a Frank-like setting by treating recursive calls as abstract commands, leaving the semantics of recursion for a handler to determine (McBride, 2015).

Conclusion
We have described our progress on the design and implementation of Frank, a language for direct-style programming with locally managed effects. Key to its design is the generalisation of function application to operator application, where an operator is n-adic and may handle effects performed by its arguments. Frank's effect type system statically tracks the collection of permitted effects and convenient syntactic sugar enables lightweight effect polymorphism in which the programmer rarely needs to read or write any effect variables.
It is our hope that Frank can be utilised as a tool for tackling the programming problems we face in real life. Whether we are writing elaborators for advanced programming languages, websites mediating exercises for students, or multi-actor communicating systems, our programming needs increasingly involve the kinds of interaction and control structures which have previously been the preserve of heavyweight operating systems development. It should rather be a joy.
Then by inversion, we have for all cases i, ∃ j. af(c, I(S a → S ))(i) = j ∧ c R ∈ ( @I, I R )(j) We perform case analysis on the derivation of , x : I R S : I .
Case I-INST-ID. Therefore, c R ∈ ( @I)(i). From our inductive hypothesis, there exists j such that: (S → s)(i) = j ∧ c R ∈ ( @I)(j) The result follows by instantiating the goal with j + 1. Case I-INST-LKP.
We have c R ∈ ( ( )@I, I R )(i) ( ). We perform a case analysis on i.
Case i = 0. Then R = R and our goal becomes: ∃ j. af(c, I(S a → S b))(0) = j ∧ c R ∈ ( @I, I R )(j) If a = b, then instantiate j to 0. Otherwise, b ∈ S and b = a. In this case, the result follows by inductive hypothesis with i = 0. Case i = i + 1.
From ( ), we have that c R ∈ ( ( )@I)(i ). Using this fact to instantiate the inductive hypothesis gives: ∃ j. (S → S )(i ) = j ∧ c R ∈ ( @I)(j) So we pick j + 1 in the goal so it suffices to show: (S a → S b)(i + 1) = j + 1