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) -> xwith type
forall (a::*) . a->aAnd using it
id Int 5Writing 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.
Enter let
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:(\ (id :: forall (a::*) . a->a) -> ... id ... id ... id ...) (\ (a::*) (x::a) -> x)What makes it awkward is that the name, id, is far away from the body, \ (a::*) (x::a) -> x. From Haskell we are more used the let and where expressions. So let's add that let. Instead of what we have above we'll write
let id :: forall (a::*) . a->a = \ (a::*) (x::a) -> x in ... id ... id ... id ...The let construct 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.
| Let Sym Type Expr ExprAdding a new constructor means that we have to modify all the functions operating on Expr, and it's extra much work because Let is a variable binding construct. For substitution we just cheat a little and use the expansion into an application and λ
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'And for normal form we use the same trick.
spine (Let i t e b) as = spine (App (Lam i t b) e) asAnd 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 yIt's just multiple nested Lets in the obvious way. Another shorthand. I'll allow the identity function to be written
let id (a::*) (x::a) :: a = x in ...The translation is pretty easy
let f (x1::t1) ... (xn::tn) :: t = b in eis
let f :: forall (x1::t1) ... (xn::tn) . t = \ (x1::t1) ... (xn::tn) -> b in eAnd 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.
let id :: forall (a::*) . a -> a; id a x = x; in ...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.
Bool
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;If the definition for Bool was written in Haskell it would be
type Bool = forall a . a->a->a false :: Bool false = \ x y -> x true :: Bool true = \ x y -> yIt would be easy to add more sugar and allow type which just means you're defining something of type *. And you could also make an omitted type in a forall mean 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;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, if takes a first argument that is the type of the branches. So when using if we 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 FalseHere is what happens:
*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->aWhat is it whining about? Expected Bool, got forall (a :: *) . a->a->a. But it says right there in the code that Bool is exactly that. What's going on?
Well, the type checker knows the type of everything, but not the value of anything. So the type checker knows that type of Bool is *, 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:
tCheck r (Let s t a e) = do tCheck r t ta <- tCheck r a when (not (betaEq ta t)) $ throwError $ "Bad let def\n" ++ show (ta, t) te <- tCheck r (subst s a e) tCheck r (Pi s t te) return teNote how we substitute the value of the definition (a) in the body before type checking it. This is a sledge hammer approach. A better one would be to change the environment in the type checker to carry values when they are known. These values could then be used when computing the normal form of an expression. But let's keep it simple for now.
Let's try again:
*CubeExpr> typeCheck' $ read "let Bool :: *; Bool = forall (a::*) . a->a->a; False :: Bool;False = \\ (a::*) (x::a) (y::a) -> x; in False" forall (a :: *) . a->a->aThat worked!
A simple top level
To make experimentation easier I've added a simple top level where you can evaluate expression, make definitions, load files, etc. The files are just a bunch of definitions the way they would go inside a let.Sample session:
Welcome to the Cube. Use :help to get help. Cube> \ (a::*) (x::a) -> x \ (a :: *) (x :: a) -> x :: forall (a :: *) . a->a Cube>When evaluating an expression it will first print the value, then ::, and finally the type.
Let's try some more. Here's the file bool.cube.
-- The Bool type. Bool :: *; Bool = forall (boolT::*) . boolT->boolT->boolT; False :: Bool; False = \ (boolT::*) (false::boolT) (true::boolT) -> false; True :: Bool; True = \ (boolT::*) (false::boolT) (true::boolT) -> true; if :: forall (a::*) . Bool -> a -> a -> a; if a b t f = b a f t; not :: Bool -> Bool; not b = if Bool b False True; and :: Bool -> Bool -> Bool; and x y = if Bool x y False; or :: Bool -> Bool -> Bool; or x y = if Bool x True y;Note how I've named some of the type variables with more suggestive names. More about that in a moment.
Cube> :load bool.cube Cube> True \ (boolT :: *) (false :: boolT) (true :: boolT) -> true :: forall (boolT :: *) . boolT->boolT->boolT Cube> and True False \ (boolT :: *) (false :: boolT) (true :: boolT) -> false :: forall (boolT :: *) . boolT->boolT->boolTBy staring carefully at these you can convince yourself that it's right. What's making it a little hard to read are all those leading lambdas (and the corresponding function types). But there's an option to suppress the printing of them.
Cube> :skip Cube> True true :: boolT Cube> and True False false :: boolTLook, it got deceptively readable.
Characters
So we can define booleans. What about something like characters? You can define those too. What is the ASCII type? It's a type with 128 different values. We saw how we can make a type with two values (Bool); we can do the same for 128. Since that takes a lot of room, I'll do it for just four values.Char :: *; Char = forall (charT::*) . charT->charT->charT->charT->charT; 'a' :: Char; 'a' = \ (charT::*) (_'a'::charT) (_'b'::charT) (_'c'::charT) (_'d'::charT) -> _'a'; 'b' :: Char; 'b' = \ (charT::*) (_'a'::charT) (_'b'::charT) (_'c'::charT) (_'d'::charT) -> _'b'; 'c' :: Char; 'c' = \ (charT::*) (_'a'::charT) (_'b'::charT) (_'c'::charT) (_'d'::charT) -> _'c'; 'd' :: Char; 'd' = \ (charT::*) (_'a'::charT) (_'b'::charT) (_'c'::charT) (_'d'::charT) -> _'d'; eqChar :: Char -> Char -> Bool; eqChar x y = x Bool (y Bool True False False False) (y Bool False True False False) (y Bool False False True False) (y Bool False False False True);Since single quotes are allowed in identifiers in my little language the 'a' is just an identifier as usual. Some sample use:
Cube> :load char.cube Cube> 'a' _'a' :: charT Cube> eqChar 'a' 'a' true :: boolT Cube> eqChar 'a' 'b' false :: boolT
A different way to use characters is just to assume that there is some character type and that is has some values. How can we do that? By using a lambda expression.
\ (Char :: *) -> \ ('a' :: Char) ('b' :: Char) ... -> ...One final little syntactic sugar is to allow lambda expressions to be written with let. So
let x :: T; in eis the same as
\ (x :: T) -> eSo for Char we can write
Char :: *; 'a' :: Char; 'b' :: Char; 'c' :: Char; 'd' :: Char; eqChar :: Char -> Char -> Bool;This is a very handy notation during program development, btw. Inside i let you can first just write the type of something and then use it freely. This something will then be lambda abstracted until such a time that you provide the definition.
Let's try the "fake" characters.
Cube> :load extchar.cube Cube> 'a' 'a' :: Char Cube> :skip Cube> 'a' \ (Char :: *) ('a' :: Char) ('b' :: Char) ('c' :: Char) ('d' :: Char) (eqChar :: Char->Char->forall (boolT :: *) . boolT->boolT->boolT) -> 'a' :: forall (Char :: *) . Char->Char->Char->Char-> (Char->Char->forall (boolT :: *) . boolT->boolT->boolT)-> Char Cube> :skip Cube> eqChar 'a' 'b' eqChar 'a' 'b' :: forall (boolT :: *) . boolT->boolT->boolTNow eqChar no longer gets evaluated, naturally, because it has no definition; it's λ bound.
Now we know how the Char type could be implemented (or supplied from the outside). It would then be all right to add it as a primitive (with the same behavior), because it will not ruin any properties. (Speaking of ruining properties, the seq function in Haskell is an example of a function that cannot be defined in the λ-calculus. And sure enough, adding it to Haskell ruined the validity of η-reduction.)
It's not difficult to extend the Expr type with some Prim constructor for primitive functions, and adding some special cases in the syntax and type checking for them. But since this is not intended to be a usable language I'll resist the temptation (for now).
Pairs
OK, booleans and characters were pretty easy. Let's do pairs next. The Pair type is parameterized over two types; the type of the first and the second component. As with booleans the representation of pairs come with the case analysis "build in". (Case analysis on pairs is flip uncurry in Haskell.) If we can correctly implement building pairs, fst, and snd we are done.Pair :: * -> * -> *; Pair a b = forall (pairT::*) . (a->b->pairT) -> pairT; PairC :: forall (a::*) (b::*) . a -> b -> Pair a b; PairC a b x y = \ (pairT::*) (pair :: a->b->pairT) -> pair x y; fst :: forall (a::*) (b::*) . Pair a b -> a; fst a b p = p a (\ (x::a) (y::b) -> x); snd :: forall (a::*) (b::*) . Pair a b -> b; snd a b p = p b (\ (x::a) (y::b) -> y);So the type is called Pair and the constructor PairC. Since they live in the same name space we can't use Haskell's convention of giving them the same name (which is (,) in Haskell.).
Again, in Haskell the first definition would be
type Pair a b = forall pairT . (a->b->pairT) -> pairTStaring at these definitions makes it obvious(?) that they work. But we can test it too.
Cube> :let S :: *; s :: S; T :: *; t :: T Cube> :load pair.cube Cube> :let p :: Pair S T; p = PairC S T s t Cube> p pair s t :: pairT Cube> fst S T p s :: S Cube> snd S T p t :: TThe :let command is used to make bindings without having to load them from a file. The first :let just introduces some values to play with.
Again, the big difference compared to Haskell is that all types are explicit. (But users of templates in C++ or generics in Java/C# might appeciate it?)
Maybe
We can follow the same pattern and define Haskell's Maybe type. In fact any non-recursive Haskell data type has a mechanical translation and easy into lambda calculus. We've seen boolean, characters, and pairs above.Maybe :: * -> *; Maybe a = forall (maybeT::*) . maybeT->(a->maybeT)->maybeT; Nothing :: forall (a::*) . Maybe a; Nothing a = \ (maybeT::*) (nothing::maybeT) (just::a->maybeT) -> nothing; Just :: forall (a::*) . a -> Maybe a; Just a x = \ (maybeT::*) (nothing::maybeT) (just::a->maybeT) -> just x; maybe :: forall (a::*) (r::*) . r -> (a->r) -> Maybe a -> r; maybe a r nothing just s = s r nothing just;
Natural numbers
Let's carry on with some natural numbers. Natural numbers are trickier because it's a recursive type. In Haskell:data Nat = Zero | Succ NatLet's try the cube version:
Cube> :load nat.cube Cube> 2 succ (succ zero) :: natT Cube> add 2 3 succ (succ (succ (succ (succ zero)))) :: natTWhat does the definitions for natural numbers look like?
Nat :: *; Nat = forall (natT::*) . natT -> (natT->natT) -> natT; 0 :: Nat; 0 = \ (natT::*) (zero::natT) (succ::natT->natT) -> zero; Succ :: Nat -> Nat; Succ n = \ (natT::*) (zero::natT) (succ::natT->natT) -> succ (n natT zero succ); natprim :: forall (r::*) . (r->r) -> r -> Nat -> r; natprim r succ zero n = n r zero succ; add :: Nat -> Nat -> Nat; add x y = x Nat y Succ; mul :: Nat -> Nat -> Nat; mul x y = x Nat 0 (add y); power :: Nat -> Nat -> Nat; power x y = y Nat (Succ 0) (mul x); isZero :: Nat -> Bool; isZero n = n Bool True (\ a::Bool -> False); 1 :: Nat; 1 = Succ 0; 2 :: Nat; 2 = Succ 1; 3 :: Nat; 3 = Succ 2;The natprim function corresponds to foldr for list and is our means of recursion. This type for natural numbers come with primitive recursion built in, just as the non-recursive data types earlier came with case analysis built in.
I'll skip defining subtraction&co, they are possible, but a little tedious (and very inefficient).
Lists
Lists are like a hybrid of natural numbers and the Maybe type. They follow the same pattern as before.List :: * -> *; List e = forall (listT::*) . listT -> (e->listT->listT) -> listT; Nil :: forall (e::*) . List e; Nil e = \ (listT::*) (nil::listT) (cons::e->listT->listT) -> nil; Cons :: forall (e::*) . e -> List e -> List e; Cons e x xs = \ (listT::*) (nil::listT) (cons::e->listT->listT) -> cons x (xs listT nil cons);And some sample functions.
foldr :: forall (a::*) (b::*) . (a->b->b) -> b -> List a -> b; foldr a b f z xs = xs b z f; map :: forall (a::*) (b::*) . (a->b) -> List a -> List b; map a b f xs = foldr a (List b) (\ (x::a) (r::List b) -> Cons b (f x) r) (Nil b) xs; append :: forall (a::*) . List a -> List a -> List a; append a xs ys = foldr a (List a) (Cons a) ys xs; foldl :: forall (a::*) (b::*) . (b->a->b) -> b -> List a -> b; foldl a b f z xs = foldr a (b->b) (\ (x::a) (g::b->b) (v::b) -> g (f v x)) (\ (x::b) -> x) xs z; reverse :: forall (a::*) . List a -> List a; reverse a xs = foldl a (List a) (\ (r :: List a) (x :: a) -> Cons a x r) (Nil a) xs;And a test:
Welcome to the Cube. Use :help to get help. Cube> :load bool.cube Cube> :load nat.cube Cube> :load list.cube Cube> :load pair.cube Cube> :load listmisc.cube Cube> :load extchar.cube Cube> :skip Cube> replicate Char 3 'b' cons 'b' (cons 'b' (cons 'b' nil)) :: listT Cube> :quitAnd with that, I'll quit too for now.
Labels: Haskell, Lambda calculus
1 Comments:
FYI, the link to the code is dead.
FWIW To anyone that is reading this, I found the source code here: https://github.com/steshaw/lennart-lambda-cube however I am not the owner of this repository
Post a Comment
<< Home