Some lambda calculus examples

Syntax

In a previous blog entry I described a simple evaluator and type checker for the lambda cube, i.e., various forms of lambda calculus.

Here I'm going to show some examples of code in pure typed λ-calculus. All the examples are typable in F ω ; the full lambda cube is not necessary.

Before doing any examples we'd better have some syntax that is not too painful, because writing λ-expression in raw Haskell is tedious. The syntax for variables, * kind, and application is easy, I'll just use Haskell syntax. For λ-expressions the Haskell syntax doesn't allow explicit type annotations, but various Haskell compiler implement that extension, so I'll just pick that. So a λ will be written, " \ (var::type) -> expr ". And as in Haskell I'll allow multiple variables between the " \ " and " -> "; it's just a shorthand for multiple lambdas.

So what about the dependent function type? The syntax (x:t)→u suggests (x::t)->u , so I'll use that. And when the variable doesn't occur we'll write t->u as usual. For type variables Haskell (well, not Haskell 98, but extensions) uses forall (a::*) . t , so I'll allow that too.

An example, the identity function:

\ (a::*) (x::a) -> x

forall (a::*) . a->a

id Int 5

Enter let

(\ (id :: forall (a::*) . a->a) -> ... id ... id ... id ...) (\ (a::*) (x::a) -> x)

id

\ (a::*) (x::a) -> x

let

where

let

let id :: forall (a::*) . a->a = \ (a::*) (x::a) -> x in ... id ... id ... id ...

let

| Let Sym Type Expr Expr

Expr

Let

sub (Let i t e b) = let App (Lam i' t' b') e' = sub (App (Lam i t b) e) in Let i' t' e' b'

spine (Let i t e b) as = spine (App (Lam i t b) e) as

with typeAnd using itWriting a pretty printer and parser for this is pretty straight forward so I'll skip that and just point you to the code . BTW, instead of using Parsec for the parser like everyone else I used ReadP. The ReadP library is very nice, partly because the alternative operator is actually commutative (unlike Parsec). But the error messages suck.Now if we want to use, say, the identity function more than once we need to name it. There is a mechanism for that, namely λ. But it looks awkward. Look:What makes it awkward is that the name,, is far away from the body,. From Haskell we are more used theandexpressions. So let's add that. Instead of what we have above we'll writeTheconstruct could be just be syntactic suger for a lambda and an application, but I've decided to add it as a constructor to the expression type instead.Adding a new constructor means that we have to modify all the functions operating on, and it's extra much work becauseis a variable binding construct. For substitution we just cheat a little and use the expansion into an application and λAnd for normal form we use the same trick.And for now, let's extend the type checker in the same way.





To make definitions a little less verbose I'll allow multiple bindings. E.g.

let x :: Int = g a; y :: Int = f x in h x y

Let

let id (a::*) (x::a) :: a = x in ...

let f (x1::t1) ... (xn::tn) :: t = b in e

let f :: forall (x1::t1) ... (xn::tn) . t = \ (x1::t1) ... (xn::tn) -> b in e

let id :: forall (a::*) . a -> a; id a x = x; in ...

Bool

It's just multiple nesteds in the obvious way. Another shorthand. I'll allow the identity function to be writtenThe translation is pretty easyisAnd finally, to make it even more like Haskell, I'll allow the type signature to be on a line of its own, omitting types on the bound variables.It's pretty easy to translate to the bare expression type. Why all these little syntactic extras? Well, remember Mary Poppins "A spoonful of sugar makes the medicine go down." Phew! Enough syntax, on to the examples.We could start by throwing in all kinds of primitive types into our little language, but who knows what might happen then. All the nice properties that have been shown about the language might not hold anymore. So instead we'll code all the types we need with what we already have.

The Bool type has two values: False and True. So we need to find a type that has exactly two different values. Fortunately that's easy: a->a->a (I'll be a bit sloppy and leave off top level quantifier sometimes, just like Haskell; That should be forall (a::*) . a->a->a ).

Why does that have two possible values? Well, we have a function type that must return an a for any possible a that it's given, so it can't conjure up the return value out of thin air. It has to return one of it's arguments. And there are two arguments to choose from, so there are two different values in that type. Here's Bool, False, True :

Bool :: *; Bool = forall (a::*) . a->a->a; False :: Bool; False = \ (a::*) (x::a) (y::a) -> x; True :: Bool; True = \ (a::*) (x::a) (y::a) -> y;

Bool

type Bool = forall a . a->a->a false :: Bool false = \ x y -> x true :: Bool true = \ x y -> y

type

*

forall

*

If the definition forwas written in Haskell it would beIt would be easy to add more sugar and allowwhich just means you're defining something of type. And you could also make an omitted type in amean type. But I've not added these little extras.

Defining the if function is trivial; it's just a matter of permuting the arguments a little, because the boolean values come with the "if" built in.

if :: forall (a::*) . Bool -> a -> a -> a; if a b t f = b a t f;

if

if

Note how the type signature is exactly the same as you'd find in Haskell. A difference is that we have explicit type abstraction and type application. For instance,takes a first argument that is the type of the branches. So when usingwe must pass in a type.

Given this we can try to type check some simple code:

let Bool :: *; Bool = forall (a::*) . a->a->a; False :: Bool; False = \ (a::*) (x::a) (y::a) -> x; in False

*CubeExpr> typeCheck $ read "let Bool :: *; Bool = forall (a::*) . a->a->a; False :: Bool;False = \\ (a::*) (x::a) (y::a) -> x; in False" *** Exception: Type error: Bad function argument type: Function: \ (False :: Bool) -> False argument: \ (a :: *) (x :: a) (y :: a) -> x expected type: Bool got type: forall (a :: *) . a->a->a

Bool

forall (a :: *) . a->a->a

Bool

Bool

*

Here is what happens:What is it whining about? Expected, got. But it says right there in the code thatis exactly that. What's going on?Well, the type checker knows theof everything, but not theof anything. So the type checker knows that type ofis, but it doesn't know what it is equal to.

The problem is that when we have a let binding we know the value of the defined variable and to be able to do dependent type checking the type checker needs to know it too. We need to change the type checking of let . Here's a simple solution: