Understand thyself.
--Aristotle
Starting with this lecture, we will attempt to eliminate meta-level interpretations beyond basic concepts that have a widely-understood meaning in mathematics (e.g. integers). The behavior that an interpreter specifies should be independent of the peculiar semantics of the metalanguage as possible. Independence guarantees that the definition exposes all of semantic subtleties of the language (instead of inheriting the subtleties of the metalanguage), making it far more useful as a defining standard.
Consider what our interpreter for LC might do when we feed it an erroneous input such as
(MEval '(5 6))Depending on which language we use to implement the interpreter, we will get different results. For instance, a Scheme implementation might report one of the following:
Error: attempt to apply non-procedure 5 apply: not a procedure (type was <fixnum>)
This is an example of meta-level inheritance, wherein the implemented language has inherited a behavior (in this case, for errors) from the implementing language. Another example of this form of inheritance is the treatment of errors in C programs. Depending on the platform (operating system and hardware) that they run on, C programs behave differently when errors arise. A language specification should define when an error arises and, for clarity, what happens when an error occurs.
Consider the following fragment of the interpreter:
((numeral? M) (numeral-n M)) ((add? M) (+ (MEval (add-lhs M) env) (MEval (add-rhs M) env))) ((lam? M) (make-closure M env)) ((app? M) (Apply (MEval (app-fun M) env) (MEval (app-arg M) env)))Where do we inherit error behavior from Scheme?
number?
) and invoke it only if
the tests succeed, signaling an error otherwise.
(define LC-+ (lambda (m n) (if (and (number? m) (number? n)) (+ m n) (LC-error ...))))
(define Apply (lambda (f a) (if (closure? f) (let ((body (closure-body f))) (MEval (lam-body body) (extend (closure-env f) (lam-arg body) a))) (LC-error ...))))
if
, to
our language that interprets 0 as "false" and non-zero integers as "true".
Then we might have the following implementation:
((If? M) (if (MEval (If-test M) env) (MEval (If-then M) env) (MEval (If-else M) env)))Unfortunately, this definition does not work: all Scheme values returned by
MEval
are acceptable in the test position
of an if
expression. Moreover, every integer, procedure,
and list, is interpreted as "true", so the failing branch will never be
taken. We could rewrite the code implementing if
as:
(if (=/= (MEval (If-test M) env) 0) ... ...)but in the process, we inherit anoteher aspect of Scheme's error-handling, since we cannot be sure the value returned by
MEval
will always be a number. We can eliminate
this dependence in exactly the same way that we did for
+
.
Answer: We have asked questions such as number?
,
which requires that the data of the metalanguage be "self-identifying".
Most programming languages do not support operations like number?
because data representations do not carry enough information to determine
what kind of data is represented! (How can you tell an pointer value
from an integer value in C or C++? Good luck!)
Just as we introduced abstract syntax to sever our dependence on the
meta-language to represent programs, we can introduce
meta-values so that we don't rely entirely on the meta-language to
represent values. We can do this with declarations such as
(define-structure (Num n)) (define-structure (Clos param body env)) (define-structure (Bool b))so now we can now write
Apply
with Clos?
(define Apply (lambda (f a) (if (Clos? f) ...)))rather than with
procedure?
.
Some semanticists argue that
this is not an entirely satisfactory solution: since
define-structure
is a complex construct specific to Scheme,
we haven't really explained much by replacing, say, number?
with
Num?
. I disagree because a collection of
define-struct
declarations simply defines a free term algebra, which is a widely understood
mathematical concept. Nevertheless,
according to the critics of define-struct
,
we could instead adopt the convention that every data object
is represented by a two-element list consisting of a tag and an value.
In this case, we would define the LC number constructor and recognizer
as follows:
(define make-Num (lambda (n) (list 'Num n)))
(define Num? (lambda (LC-val) (eq? (car LC-val 'Num))))
LC-+
?
Let us now consider our definition of Num?
. The symbol
'Num
is easily represented as a number and
eq?
is a simple hardware instruction. We still need to
explain car
.
What we just discovered is that self-identifying data rely on the meta-language's ability to allocate data dynamically. This is also true of closures and arrays. Given the importance of this concept, we will next study an explanation of dynamic allocation.