git clone /comp/105/git/lambdawhich chould give you a directory Lambda with files linterp.sml, Lhelp.ui, Lhelp.uo, Makefile, and basis.lam.
Do all six problems below.
There are three problems on
implementing the lambda calculus, which you'll do with a partner,
and three problems on
programming with Church numerals, which you'll do on your own.
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
.
//
and ending at the
next newline.
A definition can be a term, a binding, or a use
statement.
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;
-> term;
-> name = term;
-> noreduce name = term;
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.
\name.term
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;
<transcript>= -> 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.
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 (difficulty *). This problem has two parts:
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) -> 'aThese functions must 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
My solution to this problem is under 30 lines of ML code.
2. Evaluation—Substitution (difficulty **).
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.
My solution to this problem is under 40 lines of ML code.
3. Evaluation—Reductions (difficulty ***). In this problem, you use your substitution function to implement two different evaluation strategies.
My solution to this problem is under 20 lines of ML code.
4. Church Numerals—predecessor (difficulty **).
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,
<church numerals>= 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 (difficulty *). 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 (difficulty ***).
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.
\x.Mx --> M provided x is not free in MHere is an reduction in two eta steps:
\x.\y.cons x y --> \x.cons x --> consYour interpreters should be capable of this reduction.
If you test an interpreter before implementing reduction, you may see some alarming-looking terms that have extra lambdas and applications. This is because the interpreter uses lambda to implement the substitution that is needed for the free variables in your terms. Here's a sample:
<transcript of interpreter without reductions>= -> thing = \x.\y.y x; thing -> thing; (\thing.thing) \x.\y.y x
Everything is correct here except that the code claims something is in normal form when it isn't. If you reduce the term by hand, you should see that it has the normal form you would expect. A~term that refers to even more defined variables could start to look very exciting indeed.
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—Recursive functions. Although the Y combinator cannot be typed in Hindley-Milner, it is actually fairly easy to write an explicit fixed-point operator in ML. Do so. Your operator should be called fix and should have the following type:
fix : forall 'a, 'b . (('a -> 'b) -> ('a -> 'b)) -> ('a -> 'b)Use your explicit fixed-point operator to define a recursive factorial function and see if it works.
For a larger bonus, define fix without using fun or val rec (hard).
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.)
linterp.sml
for the parts on
evaluation.
church.lam
for the parts
on Church Numerals.