As you might have inferred from last week's discussion,
if you didn't remember it from the first week of 2360,
one of the oft-mentioned advantages of LISP is its
extensibility---it's very easy to add new things to the
language, and it's also very easy to build whole new
languages on top of LISP. One language that has been built
on top of LISP is called "Scheme". Scheme is used primarily
as a teaching language, and is close enough to LISP to be
considered a dialect of LISP. In fact, it's the only dialect
of LISP in current widespread use besides Common LISP.
Scheme tries to offer a minimal set of very powerful features
that can be used to implement other features.
Here are some of the ways in which Scheme is simpler than
Common LISP:
1. Fewer built-in functions and special forms (we'll tell
you what a special form is next week)
2. No special variables, only lexical variables. (i.e., no
dynamic scoping)
3. No optional parameters.
4. No special forms for looping---the programmer must use
recursion, and count on Scheme to implement it
efficiently.
5. Scheme evaluates the function part of a function call in
exactly the same way as the arguments.
Huh? What was that last one? Function calls in Scheme and
LISP are different? Yes, exactly. Here's how:
In LISP, (f x) means 1) look up the function body bound to f,
2) evaluate x, and 3) apply the retrieved function body to
the value of x. LISP doesn't evaluate f.
In Scheme, (f x) means 1) evaluate f (and we hope that will
return a function body), 2) evaluate x, and 3) apply the
result of evaluating f to the result of evaluating x. This
is a more consistent approach to evaluating an expression
than in LISP. Any expression can be in the function position
(the first of the list), and it is evaluated just like any
other argument. It's a subtle but important difference.
The mini-Scheme interpreter
A simple interpreter for Scheme is pretty easy to construct
in LISP. Here's an example of an interpreter for a subset of
Scheme:
(defun interp (x &optional env)
(cond
((symbolp x) (get-var x env))
((atom x) x)
((case (first x)
(QUOTE (second x))
(SET! (set-var! (second x) (interp (third x) env) env))
(LAMBDA (let ((parms (second x))
(code (maybe-add 'begin (rest (rest x)))))
#'(lambda (&rest args)
(interp code (extend-env parms args env)))))
(t ;; a procedure application
(apply (interp (first x) env)
(mapcar #'(lambda (v) (interp v env))
(rest x))))))))
(defun set-var! (var val env)
(if (assoc var env)
(setf (second (assoc var env)) val)
(set-global-var! var val))
val)
(defun get-var (var env)
(if (assoc var env)
(second (assoc var env))
(get-global-var var)))
(defun set-global-var! (var val)
(setf (get var 'global-val) val))
(defun get-global-var (var)
(let* ((default "unbound")
(val (get var 'global-val default)))
(if (eq val default)
(error "Unbound scheme variable: ~a" var)
val)))
(defun extend-env (vars vals env)
(nconc (mapcar #'list vars vals) env))
(defparameter *scheme-procs*
'(+ - * / = < > <= >= cons car cdr not append list read member
(null? null) (eq? eq) (equal? equal) (eqv? eql)
(write prin1) (display princ) (newline terpri)))
(defun init-scheme-interp ()
(mapc #'init-scheme-proc *scheme-procs*)
(set-global-var! t t)
(set-global-var! nil nil))
(defun init-scheme-proc (f)
(if (listp f)
(set-global-var! (first f) (symbol-function (second f)))
(set-global-var! f (symbol-function f))))
(defun maybe-add (op exps &optional if-nil)
(cond ((null exps) if-nil)
((length=1 exps) (first exps))
(t (cons op exps))))
(defun length=1 (x)
(and (consp x) (null (cdr x))))
(defun last1 (list)
(first (last list)))
(defun scheme ()
(init-scheme-interp)
(loop (format t "~&==> ")
(print (interp (read) nil))))
Let's go back and take a look at the basic evaluator
function, called "interp":
(defun interp (x &optional env)
(cond
((symbolp x) (get-var x env))
((atom x) x)
((case (first x)
(QUOTE (second x))
(SET! (set-var! (second x) (interp (third x) env) env))
(LAMBDA (let ((parms (second x))
(code (maybe-add 'begin (rest (rest x)))))
#'(lambda (&rest args)
(interp code (extend-env parms args env)))))
(t ;; a procedure application
(apply (interp (first x) env)
(mapcar #'(lambda (v) (interp v env))
(rest x))))))))
The interp function works on two arguments. The first is an
expression simply called "x", which is the expression to be
evaluated. The second is called "env" (for "environment")
which is merely an association list of variable names and
their bindings or values.
This Scheme interpreter is ready to deal with six different
cases. From top to bottom, they are:
1. If the expression is a symbol, look up its value in the
environment.
2. If the expression is an atom other than a symbol, such as
a number, just return it. Otherwise, the expression must
be a list.
3. If the list starts with QUOTE, return the quoted
expression.
4. If the list starts with SET! (same as the LISP setq),
interpret the value and then set the variable to that
value.
5. If the list starts with LAMBDA (no need for #' here),
then build a new procedure---a closure over the current
environment.
6. Otherwise, this must be a procedure application.
Interpret the procedure and all the arguments, and
apply the procedure value to the argument values.
Show me that factorial thing one more time
To get the mini-Scheme interpreter to work, load the file
containing the mini-Scheme code, and then call the function
named "scheme". The prompt will change from "?" to "==>":
Welcome to Macintosh Common Lisp Version 2.0!
?
SCHEME
? (scheme)
==>
Now you can interpret a subset of Scheme code. Some Scheme
code looks just like LISP code:
==> (+ 2 2)
4
==>
But not all Scheme code looks exactly like LISP code. Here's
how we define a new function in Scheme:
==> (set! square (lambda (x) (* x x)))
#{COMPILED-LEXICAL-CLOSURE #x5CA20E}
==>
Invoking a Scheme function is pretty much the same as
invoking a LISP function:
==> (square 2)
4
==>
We can extend the mini-Scheme interpreter easily. For
example, if we want to add a conditional like "if", we merely
add another chunk to the "case" expression in the
interpreter. This new chunk says that if the expression
being evaluated begins with IF, then evaluate the second part
of the expression by calling "interp" recursively on that.
If the value is non-nil, then call "interp" on the third part
of the expression, else call "interp" on the fourth part of
the expression. And in order to make that all happen, we
just use the LISP version of "if":
(defun interp (x &optional env)
(cond
((symbolp x) (get-var x env))
((atom x) x)
((case (first x)
(QUOTE (second x))
(SET! (set-var! (second x) (interp (third x) env) env))
(LAMBDA (let ((parms (second x))
(code (maybe-add 'begin (rest (rest x)))))
#'(lambda (&rest args)
(interp code (extend-env parms args env)))))
(IF (if (interp (second x) env) ;; {
(interp (third x) env) ;; { added stuff
(interp (fourth x) env))) ;; {
(t ;; a procedure application
(apply (interp (first x) env)
(mapcar #'(lambda (v) (interp v env))
(rest x))))))))
With that addition, we can define (what else?) the factorial
function:
==> (set! fact (lambda (x) (if (equal? x 0)
1
(* x (fact (- x 1))))))
#{COMPILED-LEXICAL-CLOSURE #x5DF56E}
==> (fact 5)
120
==>
The read-eval-print loop
One interesting side note that all you budding young LISP
hackers should be aware of is that the interactive capability
of LISP derives from something called the "read-eval-print
loop". The "read" function accepts and expression typed at
the terminal. The "eval" function is the LISP evaluator,
which evaluates the expression obtained by "read". And
"print" prints the result of the evaluation. If you nest
these three functions and stick them in the middle of a loop,
you'll get the interactivity that we've come to associate
with LISP. We can see exactly the same thing done explicitly
in our mini-Scheme interpreter, except that "eval", as noted
before, is called "interp" here:
(defun scheme ()
(init-scheme-interp)
(loop (format t "~&==> ")
(print (interp (read) nil))))
So, now that you know this, if you can build an evaluator, an
input function, and an output function, you too can create
your own interpreted programming language. This will be
handy to know if, say, you're stranded on a desert island
with a computer but no software. Well, ok, you'd need a
power source too. And maybe a big supply of Coke Classic and
a truckload of Doritos.
Macros
One of the key components in extending LISP or building new
languages on top of LISP is the "macro". A macro works like
a function, except that when LISP sees a call to a macro in
some code that is being executed, the definition of the macro
is inserted in place of the macro call. This is in contrast
to a "normal" function call which results in a copy of the
current environment being saved on the program stack,
followed by the execution of the function definition, which
in turn is followed by the "popping" of the program stack.
Another way to think of the difference between functions and
macros is this: A function produces results, while a macro
produces expressions which, when evaluated, produce results.
We've seen lots of macros in use already: dolist, dotimes, cond,
setf, do, defun, case, etc. You should look at pp. 1026-27
of Steele for an exhaustive list of predefined LISP macros.
Macros and their arguments
One big important feature of macros is that they allow you to
create constructs which do not evaluate their arguments. The
importance of this may not be immediately obvious. An
example will help you see the light. Say that you had a
version of LISP in which there was no "if" form, and you
wanted to create one. How would you do it?
Your first thought might be to create an ordinary function:
(defun my-if (test then-part else-part)
(cond (test then-part)
(T else-part)))
That looks reasonable, no? Let's test it:
? (my-if t 2 3)
2
? (my-if nil 2 3)
3
?
Perfect. Exactly what we want. Let's test it just a little
bit more:
? (my-if t (print "yes") (print "no"))
"yes"
"no"
"yes"
?
OOPS!! What happened here? Why does "my-if" do that? The
problem is that "my-if" evaluates its arguments, and these
particular arguments have side-effects when evaluated. So,
even before the "my-if" definition is applied to the
arguments, the two "print" expressions are evaluated,
resulting in both "yes" and "no" being printed on the
terminal. Only then is the function applied to the
arguments, and the result of evaluating (print "yes") is
returned. But that's not what "if" does, is it?
How then does "if" work? It's actually something called a
special form, hard-coded into the LISP compiler. You
can't write a special form; they're written only by the folks
who created the compiler. But, "if" could be implemented
as a macro, and that's something you could do yourself. How?
By using another macro called "defmacro":
(defmacro my-if (test then-part else-part)
(list 'cond (list test then-part)
(list 'T else-part)))
When LISP then sees:
(my-if nil (print "yes") (print "no"))
it first "expands the macro", replacing the macro call with:
(cond (nil (print "yes"))
(T (print "no")))
In other words, anything that's quoted is used literally, and
anything that's not quoted is replaced by the value it's
bound to at the time the macro call is encountered.
Next time we'll look at a different syntax for creating macros
that's easier on the eyes.
Copyright 1998 by Kurt Eiselt, except for the mini-Scheme
interpreter, which belongs to Peter Norvig. All other
rights reserved.
Last revised: December 1, 1998