University of Toronto
csc324, Programming Languages, Fall 1996
Assignment 1 [10% of mark]
Out: October 8, 1996
Due: October 22, 1996, 11:59 p.m.
Objective
In this project, you will learn how to evaluate arbitrary
-expressions
(both pure and applied) using S and K combinators.
In particular, we will supply you with a function compile that
translates
-expressions into S K I expressions.
You will write
Scheme functions reduce-pure and reduce-applied to interpret
S K expressions: (reduce-pure (compile E))
will be the result of evaluating E in the context of the
pure
-calculus; (reduce-applied (compile E))
will be the result of evaluating E in the context of an
applied
-calculus.
Background
A combinator is a
-expression without free variables.
Here are three combinators:
This project is based on S -combinator theory,
invented as a basis for mathematical logic in the
1920's by Schöfinkel and independently in the 1930's by Curry.
(Remember currying - a technique for producing functions
taking just one argument from functions taking two or more arguments?
That's the same guy!)
The S -combinator theory defines computation over (possibly infinite) input strings using only two operators: S and K.
However, for this project we will use three operators, S , K and
I, and thus talk about S K I expressions.
Here's the definition of S K I expressions.
- Any Scheme symbol or number is a S K I expression.
- If E is a S K I expression, then so is (E).
- Symbols S , K, and I, are S K I expressions.
- If E and F are S K I expressions, then so is
.
Nothing else is an S K I expression.
The last rule shows how to write function application -- it
is the same as in
-calculus.
As before, application associates to the left,
so that
is equal to
, which is not always the
same as
.
Given an S K I expression, we may rewrite its prefix (prefix of a string is an initial substring of this string) as follows:
Here, E, F, G, X, and Y stand for arbitrary S K I subexpressions.
[Note that
is not a subexpression of
because of left-associativity:
.]
The first rule means parentheses can be dropped when not necessary.
The second rule means that the S combinator expresses function
composition (F composed with G), passing argument X to
both functions (and therefore duplicating information). You can
prove this rewrite rule for yourself using the definition of S .
The third rule means that operator K expresses both preservation
and destruction of information -- it preserves the first argument, removes
the second, and preserves all expressions thereafter.
Since the
application associates on the left,
Finally, Iis the identity combinator. I has the same
effect as S :
So, I is not really necessary but is used anyway
to make expressions more concise. These expressions can be used to
express any computable function from strings to strings. For example,
expression S makes a duplicate of its first argument:
Notice that we reduced the first subexpression in parentheses, I x,
just as if it were a top level expression.
In another example, the expression S (S )x reduces
to three copies of x:
Project Description
It is a remarkable fact that there is a
function compile which takes an arbitrary
(pure or applied)
-calculus expression and converts it
into an equivalent S K I expression. For example,
(compile '(\ x x))
-> i
(compile '((\ x x x) 4))
-> (s i i 4)
The syntax of lambda terms is as usual, except for the
following differences.
- Applications at the top level must be surrounded by parentheses.
For example, write EF as (
).
But you may still write
as (
).
Remember that application
is left-associative, so that (
) has the same value
as
((
)
). - Use a backslash
\
in place of the Greek letter
.
(The backslash is supposed to look a bit like
.) - Spaces must appear between the lambda and the variable being abstracted.
- Spaces must appear between the variable being abstracted and the body.
Do not use a dot to separate them.
- Abstractions must be surrounded by parentheses, unless they appear as the
body of another abstraction.
Here are some examples of this syntax:Write
as (\ x + x x)
.Write
as (\ x (\ y x))
or as
(\ x \ y x)
Write
as (\ x x (\ y x))
.
Your goal is to write functions that apply S K I rewrite rules
to reduce any S K I expression to its ``normal'' form
(i.e., the expression cannot be reduced any further). When coupled with
the compile function, you will have an evaluator for the
(pure or applied)
-calculus.
In Scheme, we will represent top-level S K I applications with lists.
For example, the application S is represented in Scheme
by (s k k)
. Note that in the ordinary S K I rules, an application that
is itself an argument in an enclosing application
must always be surrounded by parentheses
in order to override left-associativity.
Stage 1
Implement the rewrite rules. We have four rewrite
rules, and thus there will be four functions.
- Function reduce-paren removes unnecessary parentheses
around the prefix of its argument. If its argument is an S K I expression
that is not an application, i.e. its argument is a list of one element,
then it
removes those parentheses:
(reduce-paren '((X ...) E ...))
-> (X ... E ...)
(reduce-paren '(x))
-> x
It will only be called when its argument actually has such unnecessary parentheses. Example:
(reduce-paren '((a b c d) a b))
-> (a b c d a b)
(reduce-paren '(42))
-> 42
- Function reduce-i will always be given a list of at least two
elements, the first of which is the name i. It returns the result
of applying the I rewrite rule:
(reduce-i '(i x ...))
-> (x ...)
Example:
(reduce-i '(i x k s y))
-> (x k s y)
- Function reduce-k will
always be given a list of at least three
elements, the first of which is the name k. It returns the result
of applying the K rewrite rule:
(reduce-k '(k x y ...))
-> (x ...)
Example:
(reduce-k '(k (x 2) q z f))
-> ((x 2) z f)
- Function reduce-s will
always be given a list of at least four
elements, the first of which is the name s. It returns the result
of applying the S rewrite rule:
(reduce-s '(s f g x ...)
-> (f x (g x) ...)
Example:
(reduce-s '(s s i x i x))
-> (s x (i x) i x)
Stage 2
Write function reduce-pure which reduces
an arbitrary S K I to its normal form
in the context of the pure
-calculus.
These are some of the auxiliary functions for
reduce-pure:
- Function reduce-pure-outer is given an S K I expression
as its one argument. If none of the rewrite rules apply to its
argument at the top
level, then it
returns its argument unchanged. Otherwise, it returns the result
of applying the appropriate rewrite rule (i.e. one of reduce-paren,
reduce-i,
reduce-k, or
reduce-s).
For example, if its argument is a list of at least two elements,
the first of which is the symbol i,
then it returns the result of passing its argument to the reduce-i
function.
Examples:
(reduce-pure-outer '(k (i y) x z (i 4)))
-> ((i y) z (i 4))
(reduce-pure-outer '((x y)))
-> (x y)
(reduce-pure-outer '(s x))
-> (s x)
In the last example, the S rule could not be applied because there
weren't enough arguments to the S function. - Function reduce-pure-whnf calls reduce-pure-outer
until no more rules can be applied. The expression
is now in what is called ``Weak Head Normal Form''.
Hint: No more reductions are possible if result of reduce-pure-outer
for some expression E equals E. Use operation equal?
to test for equality. So, you need to stop when
(equal? (reduce-pure-outer E) E)
Example:
(reduce-pure-whnf '(k i (x y) z (i 42)))
-> (z (i 42))
- Finally, function reduce-pure applies reduce-pure-whnf
to its argument.
If the result is a list, then it applies itself to all
the elements of that list.
If the result was not a list, then that is returned.
The resulting value is in ``Normal Form''.
Example:
(reduce-pure '(s (s i i) i x))
-> (x x x)
As described earlier, (reduce-pure (compile E)) for any
lambda expression E will be the normal form of E if one exists.
(However, if the normal form of E still contains
abstractions,
then they will be expressed in S K I form.)
Stage 3
We will now extend our
-calculus evaluator
to an applied
-calculus by adding rewrite rules for
some constants. The list of constants is:
if,
iszero,
pred,
succ,
fix,
and
plus.
We also have constants true, and false, although they have no
rewrite rules associated with them.
(Note that these are the same constants as described in Section 14.1 of
Sethi's book, except that we've added plus as a primitive operation
for the sake of efficiency.)
The extensions will build a function
reduce-applied which evaluates an applied S K I expression
in the context of this applied
-calculus.
First, you will write functions that implement reductions for these constants.
- Function reduce-if will
always be given a list of at least four
elements: (if cond ithen-branch ielse-branch ...).
It reduces cond
to normal form (using reduce-applied).
If the result
is the constant true, then the list (ithen-branch ...) is returned.
On the other hand,
if the result
is the constant false, then the list (ielse-branch ...)
is returned. Function reduce-if does not reduce either
ithen-branch or ielse-branch.
(reduce-if '(if true x y z))
-> (x z)
(reduce-if '(if (i false) t (i e) foo bar)
-> ((i e) foo bar)
In the first example, if chose the ``then'' branch
and appended the rest of
the expression (i.e., z) to it. In the second example,
it had to reduce the conditional to normal form before seeing that it
had value false; it then chose the ``else'' branch. - Function reduce-iszero takes a list of at least two arguments
(iszero iexpr ...).
It reduces expr
to normal form (using reduce-applied).
If the result is the number 0,
then the list (true ...) is returned.
Otherwise, the list (false ...) is returned. (Note that the rest of
the list, i.e. ... may be empty.)
For example,
(reduce-iszero '(iszero 0 x))
-> (true x)
(reduce-iszero '(iszero (i 0))
-> (true)
(reduce-iszero '(iszero 64))
-> (false)
(reduce-iszero '(iszero s))
-> (false)
- Function reduce-pred takes a list of at least two arguments
(pred iexpr ...). It reduces iexpr to normal form and
and returns the list
(iexpr-1 ...), where iexpr-1 is the result of subracting 1 from
the normal form of iexpr. The result is undefined if the normal
form of iexpr is not a number.
(reduce-pred '(pred 2 x))
-> (1 x)
(reduce-pred '(pred (i 42) x))
-> (41 x)
- Function reduce-succ works the same way as reduce-pred
but adds 1 to the second argument. For example,
(reduce-succ '(succ 2 x))
-> (3 x)
- Function reduce-plus takes a list of at least three arguments
(plus iexp1 iexp2 ...). It returns the list
(iexp1+exp2 ...) where iexp1+exp2 is the sum of the normal forms
of expressions iexp1 and iexp2.
(reduce-plus '(plus 3 5 6))
-> (8 6)
(reduce-plus '(plus (plus (i 3) (i 2)) 1)
-> (6)
- Function reduce-fix takes a list of at least two elements
(fix if ...) and returns
(if (fix if) ...).Important: reduce-fix does not evaluate if!
Example:
(reduce-fix '(fix (if true 1) y))
-> ((if true 1) (fix (if true 1)) y)
Second, you will write functions reduce-applied-outer,
reduce-applied-whnf,
and reduce-applied which are modifications
of reduce-pure-outer, reduce-pure-sequence,
and reduce-pure, respectively.
Submission (electronic)
Submit a tar file with
- Well-documented source code.
All the code for the pure reducer
should be in a file called pure.s.
The extra code needed for the applied reducer
should be in a file called pure.s. (For example, the definition
of reduce-s
should appear in pure.s but not in applied.s.)
- Test program test.s. This will load compile.s,
pure.s and
and applied.s and then run and display test cases.
We will supply you with
the source file
compile.s that contains the compile function.
Include enough test cases to test your functions.
Don't forget border cases.
- Output showing the operation of your program.
- (Optional) README file with instructions on how to run your program.
Note that we will rerun your program with, so please
do not change the output of your program!!!!!
Grading
The marking scheme will be as follows:
Testing 10
(Has the program been tested accordingly?)
Style 25
(The program should be written using functional
programming style.)
Documentation 10
(Explanation for each module and the entire program.)
Correctness 55
David Neto will grade this assignment. He will have office hours
on Friday October 11, 1996, from 4-5pm in SF3207, and on
Monday October 21, 1996, from 3-4pm in SF3207.
Notes
- You may write any auxiliary functions you find necessary,
or borrow any from compile.s.
- Severe penalties in style will be taken if you use assignments and
loops. All processing should be done using purely functional
paradigm, using recursion where appropriate.
- The minimum that you need to implement for your project to be
considered ``working'' is to implement reduce-pure
correctly.
- Code for compile and sample test cases will be
posted separately.
- Read ut.cdf.csc324h for updated information, test cases, etc.
Acknowledgment
This project was suggested by David Neto. Implementation of
function compile is also due to him.
Marsha Chechik
Thu Oct 10 13:34:43 EDT 1996