MASSACHVSETTS INSTITVTE OF TECHNOLOGY

Department of Electrical Engineering and Computer Science
6.001 -- Structure and Interpretation of Computer Programs

March 4, 1997

Lecture Notes

Today we introduce a new data type, symbol, which is used by Scheme program to represent names. This allows our programs to go beyond doing arithmetic to doing algebra: working with names, rather than values.

Besides the four properties of any first-class data type, symbols have only one new operation, eq?, which is Scheme's test for identity. If two Scheme objects are eq? then there is no way to distinguish one from the other: they are identical. We say, somewhat jokingly, that "if you paint one of them, the other changes color, too".

Just to keep in mind the distinction between variables, symbols, and strings:

(define x (list + '+ "+"))
x                                        ; ==> #[procedure +] + "+")
((car x) 2 3)                            ; ==> 5
(eq? (second x) '+)                      ; ==> #T
(eq? (third x) "+")                      ; ==> #F
(eq? (third x) '+)                       ; ==> #F

Our major example is a simple program for taking arithmetic expressions and computing their numeric value. Our language has primitive expressions that are numbers and names for values. Numbers are represented by themselves, and names are represented by symbols. We also have compound expressions that are built from a binary operator and a left expression and a right expression:

(define (make-binop op left right)
  (list left op right))
(define (binop? expression) (pair? expression))
(define (binop.left binop) (first binop))
(define (binop.op binop) (second binop))
(define (binop.right binop) (third binop))

(define number:3 3)
number:3                                 ; ==> 3
(define variable:x 'x)
variable:x                               ; ==> x
(define expr:5+10 (make-binop '+ 5 10))
expr:5+10                                ; ==> (5 + 10)
(define expr:3+7*5 (make-binop '* (make-binop '+ 3 7) 5))
expr:3+7*5                               ; ==> ((3 + 7) * 5)
(define expr:x+10 (make-binop '+ 'x 10))
expr:x+10                                ; ==> (x + 10)
(define expr:x+7*y (make-binop '* (make-binop '+ 'x 7) 'y))
expr:x+7*y                               ; ==> ((x + 7) * y)

(define (expression expr)
  (cond ((number? expr) expr)
        ((symbol? expr) (variable.value expr))
        ((binop? expr)
         (let ((op (binop.op expr))
               (rand-1 (expression (binop.left expr)))
               (rand-2 (expression (binop.right expr))))
           (cond ((eq? op '+)
                  (+ rand-1 rand-2))
                 ((eq? op '*)
                  (* rand-1 rand-2))
                 (else (error "Bad operator" expr)))))
        (else (error "Bad expression" expr))))

(define variables
  (list (cons 'X 3)
        (cons 'Y 5)))

(define (assq name a-list)
  ;; A-List looks like ((name1 ...) (name2 ...) ...)
  (cond ((null? a-list) #F)
        ((eq? name (first (car a-list)))
         (car a-list))
        (else (assq name (cdr a-list)))))

(define (variable.value name)
  (let ((entry (assq name variables)))
    (if entry
        (cdr entry)
        (error "No such variable" name))))

(expression number:3)                    ; 3
(expression variable:x)                  ; 3
(expression expr:5+10)                   ; 15
(expression expr:3+7*5)                  ; 50
(expression expr:x+10)                   ; 13
(expression expr:x+7*y)                  ; 50

If we want to change the expressions from the "infix" notation used above to Scheme's "prefix" notation, the change is simple because we have used abstract syntax to construct our compound expressions:

(define (make-binop op left right)
  (list op left right))
(define (binop? expression) (pair? expression))
(define (binop.left binop) (second binop))
(define (binop.op binop) (first binop))
(define (binop.right binop) (third binop))

(define expr:5+10 (make-binop '+ 5 10))
expr:5+10                                ; ==> (+ 5 10)
(define expr:3+7*5 (make-binop '* (make-binop '+ 3 7) 5))
expr:3+7*5                               ; ==> (* (+ 3 7) 5)
(define expr:x+10 (make-binop '+ 'x 10))
expr:x+10                                ; ==> (+ x 10)
(define expr:x+7*y (make-binop '* (make-binop '+ 'x 7) 'y))
expr:x+7*y                               ; ==> (* (+ x 7) y)
(expression expr:5+10)                   ; ==> 15
(expression expr:3+7*5)                  ; ==> 50
(expression expr:x+10)                   ; ==> 13
(expression expr:x+7*y)                  ; ==> 50

You might wonder if there's some reason why we need to build this very long cond expression to deal with new operators. Surely there's a better way?

(define (expression expr)
  (define operators
    (list (cons '+ +)
          (cons '* *)))
  (cond ((number? expr) expr)
        ((symbol? expr) (variable.value expr))
        ((list? expr)
         (let ((op (binop.op expr))
               (rand-1 (expression (binop.left expr)))
               (rand-2 (expression (binop.right expr))))
           (let ((procedure (assq op operators)))
             (if procedure
                 ((cdr procedure) rand-1 rand-2)
                 (error "Bad operator" expr)))))
        (else (error "Bad expression" expr))))

(expression expr:5+10)                  ; ==> 15
(expression expr:3+7*5)                 ; ==> 50
(expression expr:x+10)                  ; ==> 13
(expression expr:x+7*y)                 ; ==> 50

But the code for handling operators is really very similar to that for handling variables? In fact, in Scheme, operators are just ordinary expressions. Let's not go quite that far, but let's at least let them be variables in our language:

(define variables
  (list (cons 'X 3)
        (cons 'Y 5)
        (cons '+ +)
        (cons '* *)))

(define (expression expr)
  (cond ((number? expr) expr)
        ((symbol? expr) (variable.value expr))
        ((list? expr)
         (let ((op (binop.op expr))
               (rand-1 (expression (binop.left expr)))
               (rand-2 (expression (binop.right expr))))
           (let ((procedure (variable.value op)))
             (procedure rand-1 rand-2))))
        (else (error "Bad expression" expr))))

(expression expr:5+10)                  ; ==> 15
(expression expr:3+7*5)                 ; ==> 50
(expression expr:x+10)                  ; ==> 13
(expression expr:x+7*y)                 ; ==> 50

Finally, let's go back and look at what we've done with variables. The implementation is very straightforward, and uses assq which is a very efficient mechanism for dealing with association lists (dictionaries). But the variable bindings aren't very abstract. How about using a bit of data abstraction to make it cleaner:

(define make-var cons)
(define var.name car)
(define var.value cdr)

(define variables
  (list (make-var 'X 3)
        (make-var 'Y 5)
        (make-var '+ +)
        (make-var '* *)))

(define (variable.value var)
  (define (loop vars)
    (if (null? vars)
        (error "Undefined variable" var)
        (let ((first-var (first vars)))
          (if (eq? var (var.name first-var))
              (var.value first-var)
              (loop (cdr vars))))))
  (loop variables))

Did this help us? Our code is a bit more complicated than before. And it runs a bit slower. But it is more abstract, and there are some changes that can be made more easily. Like all things in life and computer science, there's no clear-cut answer!