Their paper presents first-class handlers in the setting of a language with type inference, which complicates the formal system and makes it a bit difficult to see the essential idea. In this blog post, I present the idea of first-class handlers in its simplest form, following the style of presentation in Pierce's Types and Programming Languages (TAPL). If you are familiar with variants in TAPL, think of the following as a nice improvement on them. As in TAPL, we present immutable variants, that is, there's no assignment to union fields, only initialization. (Otherwise the subtyping rules would need to be more restrictive.)
In addition to the usual types in the simply-typed lambda calculus, and the variant type, we add the type for a handler, which is like a function type but where the parameter type is fixed to be a variant type. Departing a little from the variants in TAPL, the ordering of labels in the variant and handler types does not matter.
The relevant syntax definitions are as follows.
- The expression creates (or "introduces") a union object with the union field l initialized to the value of e.
- The expression creates a handler object for label l. The expression e in the handler must evaluate to a function whose parameter type is T, the idea being that the value stored in union field l will be passed to the handler function.
- The vertical bar expression, , combines two sub-handlers into a composite handler that can handle the union of all of the cases that can be handled by the two sub-handlers.
- The case expression has two subexpressions. The first must evaluate to a union object and the second must evaluate to a handler. The case then looks at the union and invokes the appropriate sub-handler.
To get a feel for how these features work together, let's look at an example. The following code creates two union objects, two handlers, and assigns one of the two union objects to z, depending on whether w is true or false (which we leave unspecified at the moment). The code then combines the two handlers into hs and uses a case to invoke the appropriate handler for z. If w is true, then the result of this program is true (because 0 is equal to 0). Otherwise, the result is false (because not true is false).
Next we define the type system for first-class cases.
The subtyping rules are the usual ones for functions, etc. (see TAPL) plus the following subtyping rule for unions.
Next we specify the dynamic semantics of first-class cases. The reduction rule for case finds the matching handler and applies it to the union's field value v.
To specify order of evaluation, we define evaluation frames (a shallow version of evaluation contexts that I picked up indirectly from Andrew Myers lecture notes) and a reduction rule for evaluating inside a frame. We write for replacing the box inside F with the expression e.
To finish things off, it is always good to understand the values of a language, that is, the expressions that are well-typed and that cannot be further reduced. In addition to the usual value forms of the simply-typed lambda calculus, we add the following:
One last remark. This notion of first-class cases is quit similar to the categorical notion of a coproduct (or sum), where the unique arrow of a coproduct, , roughly corresponds to the vertical bar operator defined above. I had seen coproducts many times, but it wasn't until reading Blume et al.'s paper that I realized that one might actually want to have in a programming language!