from Exception Hanlders as Extensible Cases

Exception Handlers

Exceptions are an indispensable part of modern programming languages. They are, however, supported poorly in a sense that a well-typed program can unexpectedly fail due to an uncaught exception. To address this problem and moreover ensure that well-type programs do not raise uncaught exceptions, the MLPolyR type system keeps track of exceptions.

Syntax

Exception is raised by using usual raise expression, but it is handled by using a variant handle-syntax introduced by Benton and Kennedy [1]:

try x = e_1 in e_2 [handling L1 y => ex_1 | ... | Ln y => ex_n]

First, e_1 is evaluated and if it returns a value without raising exceptions, binds that to x and e_2 is evaluated. If e_1 raises an exception L_i carrying v as its payload, then ex_i is evaluated with v binded to y. If no handler for an exception raised in e_1, however, the whole expression raises it.

Example 1: Basic case

Baseline functionality consists of being able to match a manifest occurrence of a raised exception with an exactly matching handler:

try x = raise `Neg 10 in x
handling `Neg i => i
end

However, if there is any uncaught exception, the compiler rejects it:

(raise `Neg 10; 0)
Error: type mismatch: missing or extra record field Neg 

NOTE: In MLPolyR, a program is just an expression having tpye 'int'.

Example 2: first-order function case

In the following example, f 5 should return 5. However, the MLPolyR compiler rejects it because f might raise an uncaught exception:

let fun f x  = if x < 0 then raise `Neg x else x
in  f 5
end
Error: type mismatch: missing or extra record field Neg

To handle this in the type system, the function type constructor -> acquires an additional argument representing the set of exceptions that may be raised by an application. For example, the type of f becomes int-/Neg: int, .../-> int instead of int->int.

The following example does not have an uncaught exception:

let fun f x  = if x < 0 then raise `Neg x else x
in  try x = f (-1) in x
    handling `Neg i => (-i)
    end
end

Example 3: Higher-order function (and curried function) case

In case of curried functions and partial applications, we want to be able to distinguish stages that do not raise exceptions from those that might:

let fun f x zero = if x < zero then raise `Neg x else x
    val z = f 5
in  0
end

The type of f and z look like:

val f: int -> int -/Neg: int, .../-> int
val z: int -/Neg: int; ''A/-> int

However, once z is applied, an exception handler for `Neg should be placed.

We also want to be able to track exceptions through calls of higher-order functions such as map. map function applies its first argument, which is a function, to the elements of a list. Since the argument of map can raise an arbitrary set of exceptions, map must be ready to catch all exceptions. To express it, we make such a type polymorphic in the exception annotation ('a refers to a type variable and ''a refers a row-type variable):

val map: ('a -/''a/-> 'b) -> ['a] -/''a/-> ['b]

The following code shows the usage of map function:

let fun f x  = if x < 0 then raise `Neg x else x
    fun g x  = if x == 0 then raise `Zero () else x

    fun map f e = case e of
        [] => []
      | x::xs => f x :: map f xs

in try x = map f (map g [1, 0, -1])
   in 1
   handling `Neg i => -1
          | `Zero () => 0
   end
end

f, g, and map have the following types:

val f: int -/Neg: int, .../-> int
val g: int -/Zero: (), .../-> int
val map: ('a -/''a/-> 'b) -> ['a] -/''a/-> ['b]

Example 4: Exception values in a data structure

Exception values can occur in any data structure. In the following example, sum function gives the total of left components in given list. However, if any left component in the list is less than zero, its right component, which must be an exception value, is raised:

let val pr = fn n => String.output (String.fromInt n)
    val perr = String.output
    fun sum e = case e of
         [] => 0
        | (x,ex) :: rest => if x < 0 then raise ex else x + sum rest
    val data = [(2,`A "5/2007"), 
                (1,`B "6/2007"), 
                (0,`C "7/2007"), 
                (-1,`D "8/2007"),
                (-2,`E "9/2007")]

in try x = sum data in (pr x; 0)
   handling `A n => (perr n; 0)
          | `B n => (perr n; 0)
          | `C n => (perr n; 0)
          | `D n => (perr n; 0)
          | `E n => (perr n; 0)
   end
end     

Example 5: Nested exceptions

Exception values can participate in complex data flow patterns. In the following example, an exception `A carries another exception `B as its payload. The payload `B 10 itself get raised by the exception handler for `A in function f2, so a handler for `B on the call of f2 suffices to make this example exception free:

let val pr = fn n => String.output (String.fromInt n)
    val perr = String.output
    fun f1 () = raise `A (`B 10)
    fun f2 () = try x = f1 () in x
                handling `A e => raise e
                end

in try x = f2 () in 0
   handling `B n => (pr n; 0)
   end
end     

NOTE: You can run all the above examples at MLPolyR/Web

Reference

[1] N. Benton and A. Kennedy. Exceptional syntax. JFP. 2001.