Friday, June 29, 2012

First-class Cases

I recently ran across a nice paper Extensible Programming with First-Class Cases by Blume, Acar, and Chae. This paper is about a kind of type-safe disjoint union. As usual, there is a case construct that looks at a union to see which case it is and dispatches to the code for handling that case. What's different about their approach is that the handlers are first-class entities that may be defined separately from the case construct. The nice thing about having first-class handlers is that they can be used to improve modularity, for example, solving the expression problem.
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!


  1. 1) You missed an ``in'' after ``let h2 = \y:Bool.not y>''

    2) What's the difference between the first-class cases and a function which allows pattern matching on disjoint types? For example,

    let u1 = in
    let u2 = in
    let z = if w then u1 else u2 in


    h(x:Nat) = x+1
    h(y:Bool) = not y

    I suppose this snippet above can do the same thing as your example?

    3) Are ``first-class cases'' the same as ``first-class patterns''?


    1. 1) Thanks!

      2) With first-class cases, you can separately define cases and combine them in later on, in many different ways, whereas with traditional cases (which includes functions with pattern matching), you have to define the cases all in one spot. The example I gave above was meant to show the similarities, not the differences. I should add another example!

      3) I'd have to read up on first-class patterns to answer that. Which paper do you have in mind? A quick google returned a paper by Mark Tullsen and also a paper by Barry Jay.

    2. 2) Look forward to your examples :-)

      3) Yeah I was also reading them but didn't quite get the idea