Syntactic Sugar

Do-notation

A do-block consists of the layout keyword do followed by a sequence of do-statements, where

  1. do-stmt ::= pat expr [where lam-clauses]
  2. | let decls
  3. | expr
  4. lam-clause ::= pat expr

The where clause of a bind is used to handle the cases not matched by the pattern left of the arrow. See details below.

Note

Arrows can use either unicode (/) or ASCII (<-/->) variants.

For example:

  1. filter : {A : Set} (A Bool) List A List A
  2. filter p xs = do
  3. x xs
  4. true p x []
  5. where false []
  6. x []

Do-notation is desugared before scope checking and is translated into calls to _>>=_ and _>>_, whatever those happen to be bound in the context of the do-block. This means that do-blocks are not tied to any particular notion of monad. In fact if there are no monadic statements in the do block it can be used as sugar for a let:

  1. pure-do : Nat Nat
  2. pure-do n = do
  3. let p2 m = m * m
  4. p4 m = p2 (p2 m)
  5. p4 n
  6. check-pure-do : pure-do 5 625
  7. check-pure-do = refl

Desugaring

Statement

Sugar

Desugars to

Simple bind

  1. do x m
  2. m
  1. m >>= λ x
  2. m

Pattern bind

  1. do p m
  2. where p m
  3. m
  1. m >>= λ where
  2. p m
  3. p m

Absurd match

  1. do () m
  1. m >>= λ ()

Non-binding statement

  1. do m
  2. m
  1. m >>
  2. m

Let

  1. do let ds
  2. m
  1. let ds in
  2. m

If the pattern in the bind is exhaustive, the where-clause can be omitted.

Example

Do-notation becomes quite powerful together with pattern matching on indexed data. As an example, let us write a correct-by-construction type checker for simply typed λ-calculus.

First we define the raw terms, using de Bruijn indices for variables and explicit type annotations on the lambda:

  1. infixr 6 _=>_
  2. data Type : Set where
  3. nat : Type
  4. _=>_ : (A B : Type) Type
  5. data Raw : Set where
  6. var : (x : Nat) Raw
  7. lit : (n : Nat) Raw
  8. suc : Raw
  9. app : (s t : Raw) Raw
  10. lam : (A : Type) (t : Raw) Raw

Next up, well-typed terms:

  1. Context = List Type
  2. -- A proof of x xs is the index into xs where x is located.
  3. infix 2 __
  4. data __ {A : Set} (x : A) : List A Set where
  5. zero : {xs} x x xs
  6. suc : {y xs} x xs x y xs
  7. data Term : Context) : Type Set where
  8. var : {A} (x : A Γ) Term Γ A
  9. lit : (n : Nat) Term Γ nat
  10. suc : Term Γ (nat => nat)
  11. app : {A B} (s : Term Γ (A => B)) (t : Term Γ A) Term Γ B
  12. lam : A {B} (t : Term (A Γ) B) Term Γ (A => B)

Given a well-typed term we can mechanically erase all the type information (except the annotation on the lambda) to get the corresponding raw term:

  1. rawIndex : {A} {x : A} {xs} x xs Nat
  2. rawIndex zero = zero
  3. rawIndex (suc i) = suc (rawIndex i)
  4. eraseTypes : A} Term Γ A Raw
  5. eraseTypes (var x) = var (rawIndex x)
  6. eraseTypes (lit n) = lit n
  7. eraseTypes suc = suc
  8. eraseTypes (app s t) = app (eraseTypes s) (eraseTypes t)
  9. eraseTypes (lam A t) = lam A (eraseTypes t)

Now we’re ready to write the type checker. The goal is to have a function that takes a raw term and either fails with a type error, or returns a well-typed term that erases to the raw term it started with. First, lets define the return type. It’s parameterised by a context and the raw term to be checked:

  1. data WellTyped Γ e : Set where
  2. ok : (A : Type) (t : Term Γ A) eraseTypes t e WellTyped Γ e

We’re going to need a corresponding type for variables:

  1. data InScope Γ n : Set where
  2. ok : (A : Type) (i : A Γ) rawIndex i n InScope Γ n

Lets also have a type synonym for the case when the erasure proof is refl:

  1. infix 2 _ofType_
  2. pattern _ofType_ x A = ok A x refl

Since this is a do-notation example we had better have a monad. Lets use the either monad with string errors:

  1. TC : Set Set
  2. TC A = Either String A
  3. typeError : {A} String TC A
  4. typeError = left

For the monad operations, we are using instance arguments to infer which monad is being used.

We are going to need to compare types for equality. This is our first opportunity to take advantage of pattern matching binds:

  1. _=?=_ : (A B : Type) TC (A B)
  2. nat =?= nat = pure refl
  3. nat =?= (_ => _) = typeError "type mismatch: nat ‌≠ _ => _"
  4. (_ => _) =?= nat = typeError "type mismatch: _ => _ ≠ nat"
  5. (A => B) =?= (A => B₁) = do
  6. refl A =?= A
  7. refl B =?= B
  8. pure refl

We will also need to look up variables in the context:

  1. lookupVar : Γ n TC (InScope Γ n)
  2. lookupVar [] n = typeError "variable out of scope"
  3. lookupVar (A Γ) zero = pure (zero ofType A)
  4. lookupVar (A Γ) (suc n) = do
  5. i ofType B lookupVar Γ n
  6. pure (suc i ofType B)

Note how the proof obligation that the well-typed deBruijn index erases to the given raw index is taken care of completely under the hood (in this case by the refl pattern in the ofType synonym).

Finally we are ready to implement the actual type checker:

  1. infer : Γ e TC (WellTyped Γ e)
  2. infer Γ (var x) = do
  3. i ofType A lookupVar Γ x
  4. pure (var i ofType A)
  5. infer Γ (lit n) = pure (lit n ofType nat)
  6. infer Γ suc = pure (suc ofType nat => nat)
  7. infer Γ (app e e₁) = do
  8. s ofType A => B infer Γ e
  9. where _ ofType nat typeError "numbers cannot be applied to arguments"
  10. t ofType A infer Γ e
  11. refl A =?= A
  12. pure (app s t ofType B)
  13. infer Γ (lam A e) = do
  14. t ofType B infer (A Γ) e
  15. pure (lam A t ofType A => B)

In the app case we use a where-clause to handle the error case when the function to be applied is well-typed, but does not have a function type.

Idiom brackets

Idiom brackets is a notation used to make it more convenient to work with applicative functors, i.e. functors F equipped with two operations

  1. pure : {A} A F A
  2. _<*>_ : {A B} F (A B) F A F B

As do-notation, idiom brackets desugar before scope checking, so whatever the names pure and _<*>_ are bound to gets used when desugaring the idiom brackets.

The syntax for idiom brackets is

  1. (| e a .. a |)

or using unicode lens brackets (U+2987) and (U+2988):

  1. e a .. a

This expands to (assuming left associative _<*>_)

  1. pure e <*> a <*> .. <*> a

Idiom brackets work well with operators, for instance

  1. (| if a then b else c |)

desugars to

  1. pure if_then_else_ <*> a <*> b <*> c

Idiom brackets also support none or multiple applications. If the applicative functor has an additional binary operation

  1. _<|>_ : {A B} F A F A F A

then idiom brackets support multiple applications separated by a vertical bar |, i.e.

  1. (| e a .. a | e a .. a | .. | e a .. a |)

which expands to (assuming right associative _<|>_)

  1. (pure e <*> a <*> .. <*> aₙ) <|> ((pure e <*> a <*> .. <*> aₘ) <|> (pure e <*> a <*> .. <*> aₗ))

Idiom brackets without any application (|) or ⦇⦈ expend to empty if

  1. empty : {A} F A

is in scope. An applicative functor with empty and _<|>_ is typically called Alternative.

Note that pure, _<*>_, and _<|>_ need not be in scope to use (|).

Limitations:

  • Binding syntax and operator sections cannot appear immediately inside idiom brackets.

  • The top-level application inside idiom brackets cannot include implicit applications, so

    1. (| foo {x = e} a b |)

    is illegal. In case the e is pure you can write

    1. (| (foo {x = e}) a b |)

    which desugars to

    1. pure (foo {x = e}) <*> a <*> b