In the previous section our typing rules did not have a typing context. With variables we must introduce one. Recall the definition:

g := {} | g,(xref,t)

Contexts grow to the right, ensuring that the most recently defined variable is at the end of the context. Lists in functional languages grow to the left:

data List a = Nil | Cons a (List a)

This means that a List context will have the most recently defined variable at the head of the list. Idris supports SnocList, a list datatype that grows to the right:

data SnocList a = Lin | Snoc (SnocList a) a

As we did with typing information, we can also embed the typing context within our definition of Expr:

data Expr : (ctxt : SnocList Ty)
         -> (type : Ty)
                 -> Type

If we revisit our judgement form for expressions (minus the global context):

g |- e : t

you will see that both Expr and our judgement have a context and a type. The expression e, will be the data constructors.

We have also, deliberately, decided to not include names in our context. But why?

Well, alpha equivalence tells us that two or more terms are equivalent if they share the same structure and uses of variables. If we do not include variable names then we can more easily compare terms for equality. But how do we represent variables?

De Buijn indexing is when we represent variables not by their names, but to the position in the context where their types are stored. We can use dependent types and list quantifiers to record where in a list an element occurs.

Consider the following dependent datatype Elem, which is evidence that x is in sx:

data Elem : (x  :          type)
         -> (sx : SnocList type)
               -> Type
  where
    Here : (prf : x = y)
               -> Elem x (sx :< y)

    There : (ltr : Elem x sx)
                -> Elem x (sx : y)

The type constructor for Elem states that we are establishing a proposition stating the relationship between an x and a SnocList sx. The data constructors, Here and There, detail this relationship.

Here
tells us that x will be at the ‘end’ of a SnocList, where prf is definitional equality on values i.e. x and y are the same.
There
tells us that x is not at the end of the list. The argument ltr states that x must be found earlier within the list.

Elem is our proposition; Our proof that Elem is both complete and sound comes from the following decision procedure:

isElem : DecEq type
      => (x  :          type)
      -> (sx : SnocList type)
            -> Dec (Elem x sx)

We will not explore isElem in detail, aside from stating that you can find more information in Idris’ standard library. Suffice it to say, we use Elem as our nameless representation for variables.

Consider the following definition of Expr with boolean conjunction and variables.

data Expr : (ctxt : SnocList Ty)
         -> (type : Ty)
                 -> Type
  where
    B : (b : Bool) -> Expr ctxt BOOL
    And : (l : Expr ctxt BOOL)
       -> (r : Expr ctxt BOOL)
            -> Expr ctxt BOOL

    Var : (idx : Elem type ctxt)
              -> Expr ctxt type

    Let : (this : Expr  ctxt    typeA)
       -> (body : Expr (ctxt :< typeA) typeB)
               -> Expr  ctxt           typeB
Var
is our nameless variable representation, stating that the expression will have type type iff there is proof (idx) that type exists in the context i.e. is bound.
Let
introduces bound variables to the context by extending it.

We can now use Expr to write well-scoped expressions.

λΠ> Let (B True) (And (Var (Here Refl)) (B False))
Let (B True) (And (Var (Here Refl)) (B False))
λΠ> Let (B False) (Let (B True) (And (Var (Here Refl)) (B False)))
Let (B False) (Let (B True) (And (Var (Refl Here)) (B False)))

And attempt to write ill-scoped ones. For ill-scoped terms, we will write some code and get Idris to type check it:

example1 : Expr Lin BOOL
example1
  = Let (B True) (And (Var $ There Here) (B False))

which results in:

While processing right hand side of example1. When unifying:
    Expr (?sx :< BOOL) BOOL
and:
    Expr [<] BOOL
Mismatch between: ?sx :< BOOL and [<].

Compare the definition of Var with the typing rule for local variables in Olaf/Ola. Similarly compare the definition of Let with the typing rule for let-statements.

What do you notice about the translation of the rules from formal notation to Idris syntax?

Extend the definition of Expr to include support for integers and their addition.

For Expr to represent Ola expressions, you will have to remove Let.