Delayed computations

An expression can appear delayed as do e or 'e, which are both the same as _ -> e and () -> e. If e has type T, then do e and 'e both have the type forall a. a -> T.

If c is a delayed computation, it can be forced with c() or !c. The expression c must conform to a type () -> t for some type t, in which case !c has type t.

Delayed computations are important for writing expressions that require abilities. For example:

program : '{IO, Exception} ()
program = do
  use Text ++
  printLine "What is your name?"
  name = readLine()
  printLine ("Hello, " ++ name)

This example defines a small I/O program. The type {IO, Exception} () by itself is not allowed as the type of a top-level definition, since the IO and Exception abilities must be provided by a handler, see abilities and ability handlers. Instead, program has the type program : '{IO, Exception} () (note the ' indicating a delayed computation). Inside a handler for IO, this computation can be forced with !program.

Inside the program, !readLine has to be forced, as the type of readLine is readLine : '{IO, Exception} Text, a delayed computation which, when forced, reads a line from standard input.

Syntactic precedence

The reserved symbols ' and ! bind more tightly than function application, So 'f x is the same as (_ -> f) x and !x + y is the same as (x ()) + y.

These symbols bind less tightly than keywords that introduce blocks, so do x and 'let x are both the same as _ -> let x and !if b then p else q is the same as (if b then p else q) ().

Additional ' and ! combine in the following way:

  • do do syntacticPrecedence.x is the same as (_ -> (_ -> x)) or (_ _ -> x).
  • !!x is the same as x () ().
  • !'x and '!x are both the same as x.

You can use parentheses to precisely control how ' and ! get applied.