No language would be complete without the ability to create new procedures. Our new language will implement the lambda expression.
There are a few questions we need to answer before we can go about implementing lambda expressions.
An easy way to represent closures is as a variant of procval. We add a variant tagged closure to our definition of procval:
(define-datatype procval procval? (prim-proc (prim-op proc-symbol?)) (closure (ids (list-of symbol?)) (body expression?) (env environment?)))
Now we need to modify apply-proc so that it recognizes closures.
(define apply-proc (lambda (proc args) (cases procval proc (prim-proc (prim-op) (apply-prim-op prim-op args)) (closure (formals body env) (eval-exp body (extend-env formals args env))) (else (error 'apply-proc "Invalid procedure: ~s" proc)))))
We are ready for Mini-Scheme-J. The syntax is extended once more, this time to include lambda expressions. Here is the grammar for Mini-Scheme-J.
<exp> ::= <number> lit-exp (datum) | <varref> varref-exp (var) | (if <exp> <exp> <exp>) if-exp (test-exp then-exp else-exp) | (let (<decls>) <exp>) let-exp (ids rands body) | (lambda ({<id>}*) <exp>) lambda-exp (formals body) | (<exp> {<exp>}*) app-exp (rator rands) <decls> ::= <empty> | (<id> <exp>) <decls>
Parsing (lambda (x y) (+ x y)), for example, produces (in an extended version of structure-of)
#(struct:lambda-exp (x y) #(struct:app-exp #(struct:varref-exp +) (#(struct:varref-exp x) #(struct:varref-exp y))))You must also adjust the parser so that the rator field of an app-exp will accept an arbitrary expression.
Now when a lambda expression is evaluated we need to return a closure. The line we need to add to eval-exp is simply:
(define eval-exp (lambda (exp env) (cases expression exp ..... (lambda-exp (formals body) (closure formals body env)) ....)))
1. Implement Mini-Scheme-J. Almost all of the code needed to update your interpreter can be found in this section (above). All you need to do in addition is update the parser.
> (read-eval-print) MS> ((lambda (x) x) 1) 1 MS> ((lambda (x y) (* x y)) 2 4) 8 MS> (let ((sqr (lambda (x) (* x x)))) (sqr 64)) 4096 MS> (let ((sqr (lambda (x) (* x x)))) (let ((cube (lambda (x) (* x (sqr x))))) (cube 3))) 27
The let keyword you implemented earlier is a good example of what is known as syntactic sugar. That is, it could have been implemented in terms of other language features, rather than as a new language feature.
So, we didn't even need to add let as a new language feature. We could have simply instructed our parser how to translate let expressions into lambda expressions! This is a good lesson. Before implementing a new language feauture, one should always be sure that one hasn't already implemented it. : )