CS 2360 - December 1, 1998

Lecture 19 -- Scheme


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