Racket closures (= functions) can be used in other places too, and as we have seen, they can do more than encapsulate various values — they can also hold the behavior that is expected of these values.
To demonstrate this we will deal with closures in our language. We currently use a variant that holds the three pieces of relevant information:
We can replace this by a functional object, which will hold the three
values. First, change the VAL
type to hold functions for FunV
values:
And note that the function should somehow encapsulate the same
information that was there previously, the question is how this
information is going to be done, and this will determine the actual
type. This information plays a role in two places in our evaluator —
generating a closure in the Fun
case, and using it in the Call
case:
we can simply fold the marked functionality bit of Call
into a Racket
function that will be stored in a FunV
object — this piece of
functionality takes an argument value, extends the closure’s environment
with its value and the function’s name, and continues to evaluate the
function body. Folding all of this into a function gives us:
where the values of bound-body
, bound-id
, and val
are known at the
time that the FunV
is constructed. Doing this gives us the following
code for the two cases:
And now the type of the function is clear:
And again, the rest of the code is unmodified:
What we did just now is implement lexical environments and closures in the language we implement using lexical environments and closures in our own language (Racket)!
This is another example of embedding a feature of the host language in the implemented language, an issue that we have already discussed.
There are many examples of this, even when the two languages involved are different. For example, if we have this bit in the C implementation of Racket:
then the special semantics of evaluating a Racket and
form is being
inherited from C’s special treatment of &&
. You can see this by the
fact that if there is a bug in the C compiler, then it will propagate to
the resulting Racket implementation too. A different solution is to not
use &&
at all:
and we can say that this is even better since it evaluates the second expression in tail position. But in this case we don’t really get that benefit, since C itself is not doing tail-call optimization as a standard feature (though some compilers do so under some circumstances).
We have seen a few different implementations of evaluators that are quite different in flavor. They suggest the following taxonomy.
A syntactic evaluator is one that uses its own language to represent expressions and semantic runtime values of the evaluated language, implementing all the corresponding behavior explicitly.
A meta evaluator is an evaluator that uses language features of its own language to directly implement behavior of the evaluated language.
While our substitution-based FLANG evaluator was close to being a syntactic evaluator, we haven’t written any purely syntactic evaluators so far: we still relied on things like Racket arithmetics etc. The most recent evaluator that we have studied, is even more of a meta evaluator than the preceding ones: it doesn’t even implement closures and lexical scope, and instead, it uses the fact that Racket itself has them.
With a good match between the evaluated language and the implementation language, writing a meta evaluator can be very easy. With a bad match, though, it can be very hard. With a syntactic evaluator, implementing each semantic feature will be somewhat hard, but in return you don’t have to worry as much about how well the implementation and the evaluated languages match up. In particular, if there is a particularly strong mismatch between the implementation and the evaluated language, it may take less effort to write a syntactic evaluator than a meta evaluator.
As an exercise, we can build upon our latest evaluator to remove the encapsulation of the evaluator’s response in the VAL type. The resulting evaluator is shown below. This is a true meta evaluator: it uses Racket closures to implement FLANG closures, Racket function application for FLANG function application, Racket numbers for FLANG numbers, and Racket arithmetic for FLANG arithmetic. In fact, ignoring some small syntactic differences between Racket and FLANG, this latest evaluator can be classified as something more specific than a meta evaluator:
A meta-circular evaluator is a meta evaluator in which the implementation and the evaluated languages are the same.
This is essentially the concept of a “universal” evaluator, as in a “universal turing machine”.
(Put differently, the trivial nature of the evaluator clues us in to the deep connection between the two languages, whatever their syntactic differences may be.)
We saw that the difference between lazy evaluation and eager evaluation
is in the evaluation rules for with
forms, function applications, etc:
is eager, and
is lazy. But is the first rule really eager? The fact is that the only thing that makes it eager is the fact that our understanding of the mathematical notation is eager — if we were to take math as lazy, then the description of the rule becomes a description of lazy evaluation.
Another way to look at this is — take the piece of code that implements this evaluation:
and the same question applies: is this really implementing eager
evaluation? We know that this is indeed eager — we can simply try it
and check that it is, but it is only eager because we are using an eager
language for the implementation! If our own language was lazy, then the
evaluator’s implementation would run lazily, which means that the above
applications of the eval
and the subst
functions would also be lazy,
making our evaluator lazy as well.
This is a general phenomena where some of the semantic features of the language we use (math in the formal description, Racket in our code) gets embedded into the language we implement.
Here’s another example — consider the code that implements arithmetics:
what if it was written like this:
Would it still implement unlimited integers and exact fractions? That depends on the language that was used to implement it: the above syntax suggests C, C++, Java, or some other relative, which usually come with limited integers and no exact fractions. But this really depends on the language — even our own code has unlimited integers and exact rationals only because Racket has them. If we were using a language that didn’t have such features (there are such Scheme implementations), then our implemented language would absorb these (lack of) features too, and its own numbers would be limited in just the same way. (And this includes the syntax for numbers, which we embedded intentionally, like the syntax for identifiers).
The bottom line is that we should be aware of such issues, and be very careful when we talk about semantics. Even the language that we use to communicate (semi-formal logic) can mean different things.
Aside: read “Reflections on Trusting Trust” by Ken Thompson (You can skip to the “Stage II” part to get to the interesting stuff.)
(And when you’re done, look for “XcodeGhost” to see a relevant example, and don’t miss the leaked document on the wikipedia page…)
Here is yet another variation of our evaluator that is even closer to a meta-circular evaluator. It uses Racket values directly to implement values, so arithmetic operations become straightforward. Note especially how the case for function application is similar to arithmetics: a FLANG function application translates to a Racket function application. In both cases (applications and arithmetics) we don’t even check the objects since they are simple Racket objects — if our language happens to have some meaning for arithmetics with functions, or for applying numbers, then we will inherit the same semantics in our language. This means that we now specify less behavior and fall back more often on what Racket does.
We use Racket values with this type definition:
And the evaluation function can now be:
Note how the arithmetics implementation is simple — it’s a direct translation of the FLANG syntax to Racket operations, and since we don’t check the inputs to the Racket operations, we let Racket throw type errors for us. Note also how function application is just like the arithmetic operations: a FLANG application is directly translated to a Racket application.
However, this does not work quite as simply in Typed Racket. The whole
point of typechecking is that we never run into type errors — so we
cannot throw back on Racket errors since code that might produce them is
forbidden! A way around this is to perform explicit checks that
guarantee that Racket cannot run into type errors. We do this with the
following two helpers that are defined inside eval
:
Note that Typed Racket is “smart enough” to figure out that in evalF
the result of the recursive evaluation has to be either Number
or
(VAL -> VAL)
; and since the if
throws out on numbers, we’re left
with (VAL -> VAL)
functions, not just any function.
There is one major feature that is still missing from our language: we
have no way to perform recursion (therefore no kind of loops). So far,
we could only use recursion when we had names. In FLANG, the only way
we can have names is through with
which not good enough for recursion.
To discuss the issue of recursion, we switch to a “broken” version of
(untyped) Racket — one where a define
has a different scoping rules:
the scope of the defined name does not cover the defined expression.
Specifically, in this language, this doesn’t work:
In our language, this translation would also not work (assuming we have
if
etc):
And similarly, in plain Racket this won’t work if let
is the only tool
you use to create bindings:
In the broken-scope language, the define
form is more similar to a
mathematical definition. For example, when we write:
it is actually shorthand for
we can then replace defined names with their definitions:
and this can go on, until we get to the actual code that we wrote:
This means that the above fact
definition is similar to writing:
which is not a well-formed definition — it is meaningless (this is a
formal use of the word “meaningless”). What we’d really want, is to take
the equation (using =
instead of :=
)
and find a solution which will be a value for fact
that makes this
true.
If you look at the Racket evaluation rules handout on the web page, you
will see that this problem is related to the way that we introduced the
Racket define
: there is a hand-wavy explanation that talks about
knowing things.
The big question is: can we define recursive functions without Racket’s
magical define
form?
Note: This question is a little different than the question of implementing recursion in our language — in the Racket case we have no control over the implementation of the language. As it will eventually turn out, implementing recursion in our own language will be quite easy when we use mutation in a specific way. So the question that we’re now facing can be phrased as either “can we get recursion in Racket without Racket’s magical definition forms?” or “can we get recursion in our interpreter without mutation?”.