Conversation
|
I'd like to ask a question. Is this a step on the path to fully typed effects, or is it a stepping-stone to "typed effects on the cheap", as you mention? |
|
Typos in the translations to and from named effect handlers:
|
|
I am generally in favour of this sort of change. As @lpw25 points out, it is important to conceptually distinguish between effects and their operations. Whilst this change aligns better with the literature of algebraic effects (the mathematical term; not the (abused and misused) programming term), it also opens the door for performing optimisations that would be important in effect handler-oriented programs by which I mean programs that makes heavy use of effect handlers -- such optimisations are perhaps less important or quite possibly even irrelevant if the program has only one or two handlers installed at a time. More importantly, I see this as a stepping-stone towards a safer API for programming with handlers in OCaml and eventual type-and-effect system support. It is worth noting that this API is less expressive in some sense than the current API in trunk. As I understand it, the proposed API forces the programmer to handle the same set of effects at each "step", whereas the trunk API allows the programmer to change the set of effects entirely! Thus the proposed API offers some stronger structural guarantees than the trunk API, which can be utilised to better reason about and optimise programs. My understanding of proposed API is that it gives a shallow view onto the underlying free monad of the computation (the My only criticism is rather shallow (pun intended). I wish we would not use the term "shallow handlers" here. The proposed API does not offer shallow handlers in the sense of the literature. A shallow handler provides dynamic control (in the sense of dynamic delimited control), whereas the proposed API provides static control (the difference between dynamic and static control is whether the invocation of a continuation is guarded by a delimiter, i.e. whether the delimiter is reinstated with along with the continuation). Thus there is a fine semantic difference between shallow handlers and the ones proposed here/the ones in trunk. Alas, I do not know of a better name. As it stands this construct has no name in the literature. Colloquially, we call them "sheep" handlers (a name due to @stedolan). Though, I do not know whether we want to refer to them formally as such. Anyways, this is not a hill I am willing to die on, so feel free to ignore this rant. |
I don't think this is actually the case. The current API delegates the task of "matching" on an operation on user-level OCaml code, and that has a cost. But this is not required by the programming model where each operation is defined on its own. We could have an API that provides a registration function for each operation (or a compilation scheme, if this is user syntax), and marks this handler on the stack so that one can efficiently jump to the handler without running OCaml code. |
Apologies, I am not sure what "this" is bound to. I don't see how what I said is at odds with your suggestion.
This sounds like the scheme in the evidence translation paper. |
|
To clarify: I am skeptical that "separating effects from operations" has a strong impact on the optimization opportunities. |
Is it fair to say that separating effects from operations is sufficient for the optimisation where we avoid examining handlers which are known not to handle an effect, but such a separation is not necessary? |
|
I've only experimented with effects with local toy examples, sometimes online here and there, without complete formal understanding of them. I feel like this proposal makes reasoning about effects better for me when doing learning-by-analogy, with comparisons to other languages supporting them. I'm vouching for the pedagogical point raised in the discussion. And I can see how the jump from this to effect types would be gentle. This is very attractive change, But how does it interact with the recent addition of effects syntax in trunk? |
Introduction
This PR proposes an alternative API for effects. Currently, the effect API is based around a single type of operations
'a Effect.tindexed by their return types (or arity in the literature). The core of this proposal is to replace this type by one that represents effects and is indexed by their effect type, which describes the operations of that effect.The difference is best illustrated by an example. Currently, defining a shallow effect handler for integer state is done as follows:
The operations of state --
GetandSet-- are added to the type of operations. Then a handler is defined which handles those two operations. The call tocontinue_with(or indeedfiber) does not indicate which operations will be handled. The set of operations handled is represented by whether theeffcfunction returnsSomeorNone.With the proposed API, the same handler is implemented by:
Here we start by defining a new
int_stateeffect type, describing the operations that make up the concept of integer state. Then we define a new effectcounterthat has that effect type. Then we define a handler for the counter effect. The call torunis explicitly passedcounteras the effect to be handled.You can see the API itself in the effect.mli file.
This PR doesn't include syntax for deep handlers as proposed in #12309. I have a half-finished branch for that on top of this API, but it needs a few more days work to finish.
Advantages
I think there are numerous advantages to the proposed API. I'm not necessarily claiming that it is impossible to achieve these things with the current API, but it is much more natural in the proposed one.
Aligns better with the theory
The proposed API is much closer to the theory of algebraic effects, where each algebraic effect is defined by a set of operations and equations on those operations.
Whilst not the most important advantage in itself, I think that in some sense the other advantages mostly stem from this one. If we only have a single type of operations we lose the ability to talk about actual algebraic effects and their operations. We cannot easily express the idea of a morphism between effects, and we cannot easily map between different representations of the same effect.
In addition to conflating effects and operations, the current API also conflates the mechanism for referring to an effect handler -- in the case of both these APIs that is a dynamically created value of type
'a Effect.t-- with the type of the effect handler -- i.e. it's operations. Computations using algebraic effects are a form of open term -- i.e. a term with free variables -- and conflating these two concepts is equivalent to conflating the name of a free variable with the type of that variable. This is discussed further in a recent talk I gave.Exhaustivity checking
Associating operations with their effect allows us to talk about a handler for that whole effect, and gives exhaustivity warnings if a case is forgotten.
Currently, code like this give no error:
continue_with k x { retc = (fun v -> v, state); exnc = (fun e -> raise e); effc = (fun (type b) (eff : b t) -> match eff with | Get -> Some (fun (k : (b,r) continuation) -> loop state k state) | Set 0 -> Some (fun (k : (b,r) continuation) -> loop new_state k ()) | e -> None) }even though the handler does not handle
Setfor values other than0.Similarly, if we wish to add a new operation to an effect we must manually attempt to find all handlers for that effect and update them.
Supports different representations of effectful computation
By separating out the notion of effect type we can relate different representations of effectful computation.
For example, here is an implementation of "freer" monads:
And here is a function to translate that representation into the proposed effect API:
As another example, here is a simple API for named effect handlers:
Named handlers work by giving you a fresh name to refer to each handler directly -- the
handlertype above -- and then the user threads that value down to each corresponding use ofperform. It's similar to systems based on capabilities.And here are translations between named effect handlers and this proposed API:
Note that all these transformations are completely generic in the type of the effect, which relies on having something in the type system that represents that type.
I'm particularly interested in supporting named effect handlers: using the
localmode described in this blog post and this talk you can prevent unhandled effect errors, essentially giving a system of typed effects on the cheap.Allows a simple shallow handler API
Separating out effect types from effects allows us to provide the a simple shallow handler API based on the
steptype:as opposed to the more complicated
continue_withof the current API:This allows for much cleaner implementations of handlers, like the one for integer state in the introduction.
The simpler API relies on being able to specify the effect being handled explicitly in advance rather than every handler potentially handling all effects.
Easily supports multiple instances of the same effect
It is often useful to have multiple instances of the same effect. For example, using two pieces of state simultaneously. Such instances also often have parameterised effect types. This is awkward to represent currently, requiring functors (or first-class modules) over effect constructor definitions.
For example, here is the definition and a use of a generic handler for state in the current API:
With the proposed API this can be implemented as:
Supports general functions for manipulating effects
Separating effect names from effect types allows you to have general functions for manipulating effect names. For example, you can have a function that handles any effect by renaming it to another one:
Supports effect type abstraction
Separating effect names from effect types allows you to make the effect type abstract without necessarily making the name abstract. Without an effect system this distinction doesn't matter too much, and indeed there isn't even a static notion of equality on effect names yet, but if we ever attempt to track effects in the type system this distinction is very important. This paper tries to allow effect abstraction without that distinction and you can see how complicated it gets.
Better for teaching algebraic effects
This is clearly more subjective, but I believe that this API is much better for teaching algebraic effects and handlers.
Having a language-level notion of an effect type allows you to discuss the meaning of a particular effect. You can show the effect type for e.g.
stateand discuss its operations and the equations between those operations. That in turn allows you to discuss the relation to algebras and algebraic data-types.The ability to translate between different representations of effectful computation makes it easier to discuss those other representations. It should fit much more naturally into a course also covering e.g. free monads.
I also think that the simpler shallow API is a little easier to grasp than the existing APIs or syntax for deep handlers. You just run a computation until it returns, raises or performs.
Supports more efficient implementation
Since the handled effect name is known in advance the matching logic can all be moved into the runtime, which should be noticeably more efficient. I haven't actually implemented this yet as I wanted to keep the diff on the runtime as small as I could.
Details
This PR is on top of #12735, and the first two commits come from that PR. The next commit makes an error message more precise so that it will still make sense with effect types. The next three commits adds the
operationtype andeffecttype declarations.The
operationtype has two type parameters, the first is the return type of the operation, the second is the effect type to which the operation belongs.effecttype declarations have the form:As with variant types, they simultaneously declare a type and some constructors. Unlike a variant type, the constructors produce values of the
operationtype rather than the effect type directly. The types can have type parameters, which can be referred to in the types of the operations.The operations can be polymorphic. For example:
defines an effect whose
Chooseoperation is polymorphic in'a. The type-checking for these is essentially the same as for existentials in a variant type.The last two commits add the new effects API. The old API is implemented using the new one and made available as
Effect.Legacy. I've made three copies of the tests that were intests/effects, one intests/effects-legacyusing the old API, one intests/effects-deepusing deep handlers in the new API and one intests/effects-shallowusing shallow handlers in the new API.Note that the deep handlers API is much more usable in combination with #12732. For instance, this scheduler in the old API without that PR:
becomes: