CS152 Homework Lambda

CS152 Assignment: Lambda Calculus

Due Tuesday, April 3, at 11:59PM.

Setup and Instructions

In your cs152 directory create a subdirectory called [[lambda]]. You will need to copy the files linterp.sml, Lhelp.ui, Lhelp.uo, Makefile, and basis.lam from directory ~cs152/software/lambda.

Do all six problems below. There are three problems on implementing the lambda calculus and three problems on programming with Church numerals. For problems 1-3, modify the [[linterp.sml]] file. Put your answers for the Church numerals problems, 4-6, in a file called [[church.lam]].

Introduction to the Lambda interpreter

You will be working with a small, interactive interpreter for the lambda calculus. This section explains what syntax to use and how to interact with the interpreter.

Concrete syntax

All toplevel items in the lambda interpreter must be terminated with a semicolon. Comments are C++ style line comments, starting with the string [[//]] and ending at the next newline. A toplevel item can be a term, a binding, or a [[use]] statement.
A [[use]] statement loads a file into the interpreter as if it had been typed in directly. A [[use]] statement is of the form
[[-> use filename;]]
When a term is entered at the toplevel, any free variables in the term which appear in the environment are substituted with their bindings in the environment, then the term is reduced to normal form (if possible) and the result is printed.
[[-> term;]]
A binding first evaluates the term on the right hand side as above, and then binds the name on the left hand side to the resulting term in the environment. It is an error for the term on the right hand side to contain any free variables which are not bound in the environment. Bindings are of the form:
[[-> name = term;]]
[[-> noreduce name = term;]]
If the [[noreduce]] keyword appears, the term on the right-hand side is not normalized. This tactic can be useful for terms that have no normal form, such as
noreduce bot = (\x.x x)(\x.x x);
noreduce Y   = \f.(\x.f(x x))(\x.f(x x));

A lambda term can be either a variable, a lambda abstraction, an application, or a parenthesized lambda term. Precedence is as in ML. A lambda abstraction is written as follows. Note that each lambda abstraction can abstract over only one variable.
Application of one term to another is written:
[[t1 t2]]

The lambda interpreter is very liberal about names of variables. A name is any string of characters that contains neither whitespace, nor control characters, nor any of the following characters: [[\]] [[(]] [[)]] [[.]] [[=]] [[/]]. Also, the string [[use]] is reserved and is therefore not a name. So for example, the following are all legal:

1    = \f.\x.f x;
True = \x.\y.x;
one  = True 1;

A short example transcript

A healthy lambda interpreter should be capable of something like the following: <>= -> true = \x.\y.x; -> false = \x.\y.y; -> pair = \x.\y.\f.f x y; -> fst = \p. p (\x.\y.x); -> snd = \p.p(\x.\y.y); -> true; \x.\y.x -> fst (pair true false); \x.\y.x -> snd (pair true false); \x.\y.y -> if = \x.\y.\z.x y z; if -> (if true fst snd) (pair false true); \x.\y.y -> (if false fst snd) (pair true false); \x.\y.y @ For more example definitions, see the [[basis.lam]] file distributed with the assignment.

Modifying the Lambda Interpreter

The purpose of these problems is to help you learn about substitution and reduction, the fundamental notions of the lambda calculus. We also give you a little more practice in continuation passing, which is an essential technique in lambda-land.

For these problems, define appropriate types and functions in [[linterp.sml]]. When you are done, you will have a working lambda interpreter. Some of the code we give you (Lhelp.ui and Lhelp.uo) is object code only, so you will have to build the interpreter using Moscow ML. Just typing make should do it.

1. Evaluation---Basics [15 points]. This problem has two parts:

  1. Using ML, create a type definition for a type term, which should represent a term in the untyped lambda calculus. Using your representation, define the following functions with the given types:
      lam : string -> term -> term    (* lambda abstraction *)
      app : term -> term -> term      (* application        *)
      var : string -> term            (* variable           *)
      cpsLambda :                     (* observer *)
        term -> 
        (string -> term -> 'a) -> 
        (term -> term -> 'a) -> 
        (string -> 'a) -> 
    These functions should obey the following algebraic laws:
      cpsLambda (lam x e)  f g h = f x e
      cpsLambda (app e e') f g h = g e e'
      cpsLambda (var x)    f g h = h x

  2. Using cpsLambda, define a function toString of type term -> string that converts a term to a string in uScheme syntax. Your toString function should be independent of your representation. That is, it should work using the functions above.

Our solution to this problem is under 30 lines of ML code.

2. Evaluation---Substitution [11 points]. Implement substitution on your term representation. Use a function subst of type string * term -> term -> term. To compute the substitution M[x|–>N], you should call subst (x, N) M.
Also define a function [[freeVars]] of type term -> string list which returns a list of all the free variables in a term.

Our solution to this problem is under 40 lines of ML code.

3. Evaluation---Reductions [24 points]. In this problem, you use your substitution function to implement two different evaluation strategies.

  1. Implement normal-order reduction on terms. That is, write a function reduceN : term -> term that takes a term, performs a single reduction step (either beta or eta) in normal order, and returns a new term. If the term you are given is already in normal form, your code should raise the exception NormalForm, which you should define.

  2. Implement applicative-order reduction on terms. That is, write a function reduceA : term -> term that takes a term, performs a single reduction step (either beta or eta) in applicative order, and returns a new term. If the term you are given is already in normal form, your code should raise the exception NormalForm, which you should reuse from the previous part.

Our solution to this problem is under 20 lines of ML code.

Work with Church Numerals

The purpose of the problems below is to give you a little practice programming in the lambda calculus. Our solutions to all three problems total less than fifteen lines of code.

4. Church Numerals---predecessor [15 points]. Implement the predecessor function for the Church numerals. That is, a function [[pred]] such that [[pred(succ n)=n]], and [[pred(0)=0]]. Ultimately, you will write your function in lambda notation acceptable to the lambda interpreter, but you may find it useful to try to write your initial version in Typed uScheme (or ML or uScheme) to make it easier to debug. To get full credit, you must explain the reasoning behind your answer.

Remember, <>= 0 = \f.\x.x; succ = \n.\f.\x.f (n f x); + = \n.\m.n succ m; * = \n.\m.n (+ m) 0; @ You can load the initial basis with these definitions already created by typing: [[use basis.lam;]] in your interpreter.

Hint: Define a function [[lag]], so that [[lag(n,m) = (n+1,n)]]. Now, what is [[lag(lag(lag(lag(0,0))))]]?.

5. Church Numerals---equality [15 points]. Assume you have a predecessor function. Use it to implement an equality function on Church numerals. Again, turn in your answer using (untyped) lambda notation, but feel free to start off in uScheme or Typed uScheme if that makes it easier to debug. (Don't expect to be able to use ML here; the Hindley-Milner type system is not powerful enough to express the types of all of the functions you need to write.) Do not use recursion or the Y-combinator in your solution.

Remember the definitions of the Booleans: <<*>>= true = \x.\y.x; false = \x.\y.y; @ Hints: Write a function to test if a Church numeral is zero. Consider that n = m if and only if n <= m and m <= n. Remember to use the predecessor function.

6. Church Numerals---division and modulus [20 points]. Write a function [[divmod]] such that given two Church numerals [[m]] and [[n]], [[divmod m n]] returns a lambda-term representing the pair (m div n, m mod n). Again, use pure untyped lambda notation. You may use [[pair]], [[fst]], and [[snd]] as defined in class, and you will use the functions from the previous problems. You may not use explicit recursion; if you want a recursive solution, use the Y combinator.

Extra Credit

Extra Credit---Normalization. Write a higher-order function that takes as argument a reducing strategy (e.g., reduceA or reduceN) and returns a function that normalizes a term. Your function should also count the number of reductions it takes to reach a normal form. As a tiny experiment, report the cost of computing using Church numerals in both reduction strategies. For example, you could report the number of reductions it takes to reduce ``three times four'' to normal form.

This function should be doable in about 10 lines of ML.

Extra Credit---Normal forms galore. Discover what Head Normal Form and Weak Head Normal Form are and implement reduction strategies for them. Explain, in an organized way, the differences between the four reduction strategies you have implemented.

Extra Credit---Typed Equality. For extra credit, write down equality on Church numerals using Typed uScheme, give the type of the term in algebraic notation, and explain why this function can't be written in ML. (By using the ``erasure'' theorem in reverse, you can take your untyped version and just add type abstractions and type applications.)

What to submit

For this assignment, please submit [[church.lam]] for the parts on Church Numerals and your modified [[linterp.sml]] for the parts on evaluation.

Submit using submit-lambda from nice.