This assignment is to be done in Racket (#lang plai
).
You are encouraged to do this in teams of two, both
students working together on all parts of the assignment;
if you do this, turn in one homework with both names on it.
As always, every function must have a contract and test cases, and
I expect that you've tested every function before turning it in to me.
Any user errors you can catch in the parser should be caught
there, rather than waiting for run-time (the interpreter), such as
calling a built-in binary operator on the wrong number of arguments,
or a with
with the wrong syntax, or
referring to an undefined variable, or
calling a "user-defined" function that isn't defined.
Modify your WAE or BWAE program from Project 1 so it implements first-class functions as described in Chapter 6 of PLAI. There will be two new types of expression: an (anonymous) function definition, analogous to "lambda" in Scheme, and a function call. Here's an example of the concrete syntax:
{{fun {y} {* y y}} 5}In this example, we've created an anonymous function which squares things, and we've called that function on the argument 5.
It should also be possible to store functions in variables, e.g.
{with {{sqr {fun {x} {* x x}}}} {+ {sqr 3} {sqr 4}}}would define a
sqr
function, apply it to 3 and 4,
and return the sum of the results.
For this part of the assignment, functions will only take exactly one parameter,
and the interp
function (what used to be named calc
)
should work by substitution (not deferred).
The PLAI book does most of the work for you; you'll mostly just need to write
test cases and the parser.
Call this language FWAE or FBWAE (depending on which language you started with).
In Project 2, you've already implemented conditionals (ifpos
),
deferred substitution, and multi-parameter functions. Add these features to
the language with first-class functions, in whatever order you like (with the
goal of getting all three working, of course).
As pointed out in Chapter 6, combining deferred substitution with first-class functions requires introducing something called a "function closure", which wraps up a function together with the values of variables that were in effect at the time the function was defined. Thus for example
{with {f {with {x 5} {fun {y} {+ x y}}}} {with {x 7} {f 3}}}should evaluate to 8, not 10, because
f
is actually defined as
a function closure combining {fun {y} {+ x y}}
with the fact that
x=5
.
A variable can now hold data of two different types: a number or a function closure, and an expression can evaluate to either of those two things. You may at this point want to separate the run-time types "number" and "function closure" from the abstract syntax variants "num" and "fun" -- or, if you prefer, you can merge them together, and have the parser build function closures with empty variable lists.
It doesn't make sense to multiply two function closures,
or multiply a number by a function closure, and it doesn't make sense to "call" a
number on arguments. It also doesn't make sense to call a function closure
of 3 parameters on only 1 argument. Any of these things should be flagged as
errors. Some such errors may be detectable by the parser,
but in general you'll have to check for them at run-time (i.e.
in interp
).
One error that can (and should!) be caught by the parser is
a function definition with the same parameter name appearing more than
once, just as a multi-branched with
shouldn't be allowed to
bind more than one variable of the same name.
Since we now have an easy way to define functions within the language,
we no longer need interp
to take in a list of function
definitions; it can just take in an expression and an environment.
Call this language MPDCFMBWAE, or something to indicate which features you've actually implemented.
Answer the discussion questions 6.2.1, 6.2.2, 6.2.3.
(Another side effect of defining named functions in the language,
using with
and fun
and function closures, is
that recursion becomes trickier. Our current semantics for with
tell us to evaluate the bound-value before defining the new variable,
so
{with {x 5} {with {x {+ x 2}} x}}returns 7 rather than returning 5 or going into an infinite loop or something. However, this means that the definition
{with {fact {fun {n} {ifpos n {* n {fact {- n 1}}} 1}}} {fact 4}}fails because
fact
isn't defined yet when we build the function
closure. We'll see ways around this in chapters 9-10, next semester.)
with
Do exercise 6.3.1, writing a preprocessor that
takes an FWAE expression containing with
's and produces an
equivalent expression that doesn't contain any with
's.
(If you've got multi-parameter functions working, it should be easy to
transform multi-branched with
s. If not, just deal with
single-branched with
s.)
Note: This means that any programming language that has
lambda
doesn't need local variables.
Write this as a function named something like eliminate-with
,
and add it to the MPDCFMBWAE file from the previous stage.
Answer the discussion questions 6.4.1, 6.4.2.
For each phase of the assignment, turn in the Racket source code for
your abstract-syntax definition, parser, interpreter, and any helper
functions they need. Each function must have a contract and test
cases (using test
).
As mentioned above, eliminate-with
is just a function (and perhaps
some helper functions) which can be added to the source file for the last,
most inclusive language you've got.