COS 441 - Procedural Abstraction and Syntactic Abstraction - Feb 15, 1996

Induction, Recursion, Programming

Parse trees, *lists, and lists are inductively defined structures. To operate over an inductively defined structure, write a recursive procedure that examines each of definition. We have seen several examples of such procedures already.

Let's take a look at the natural numbers defined inductively.

N ::= 0 | succ N

(define even? 
  (lambda (n)
     (cond ((zero? n) #t)
           (else (odd? (- n 1))))))

(define odd ...)
Repeated patterns in programming are bad because each repetition offers another opportunity to introduce a bug. Use procedures to abstract common patterns. Consider the problem of adding one to each element of a list.
(define add-each
   (lambda (l)
      (cond ((null? l) '())
	    (else (cons (add1 (car l)) (add-each (cdr l)))))))
Suppose we also need to subtract one from every element.
(define sub-each
  (lambda (l)
     (cond ((null? l) '())
	   (else (cons (sub1 (car l)) (add-each (cdr l)))))))
These procedures are nearly the same. We can write one procedure, abstracting over the operation:
(define each
  (lambda (f l)
    (cond ((null? l) '())
	  (else (cons (f (car l)) (each (cdl l)))))))
This procedure is map.

Now consider multiplying all the numbers of a list together.

(define *-overlist
   (lambda (l)
      (if (null? l) 1
	  (* (car l) (*-overlist (cdr l))))))
If we also wanted to define +-overlist we should use procedure abstraction. To do this we write a folding function. Folding can occur either to the left or to the right. The procedure above folds to the right.
(define foldr
  (lambda (f i l)
    (if (null? l) i
	(f (car l) (foldr f i (cdr l))))))

(define *-overlist
  (lambda (l)
    (foldr * 1 l)))
foldr multiplies elements from right to left. We can also build a fold function that works left to right.
(define foldl
  (lambda (f i l)
    (if (null? l)
        i
        (foldl f (f i (car l)) (cdr l)))))
The direction of the fold does not matter in the case of a communicative operation. But if the function is not communicative or has side effects the direction of the fold is quite important. Note how we can now define map using foldr.
(map f l) = (foldr (lambda (a b) (cons (f a) b)) '() l)
Much of this we can also do in C, but suppose we can define a function that adds n to its parameter in the following manner.
(define add-n
  (lambda (n)
     (lambda (m)
       (+ m n))))

(map (add-n 3) '(1 2 3))
This example shows that we can construct open procedures that capture the value of bound variables. You can not do this in C, hence procedures like map and fold are less useful in C.

Syntactic Abstraction

Convince yourself that the following are equivalent.

(lambda (x y)
  (let ((x y)
	(y x))
    x)))

(lambda (x y)
  ((lambda (x y) x)
   y
   x))
This example shows that let is a syntactic abstraction. In general
  (let ((x[1] e[1]) ... (x[n] e[n])) e')
= ((lambda(x[1] .. x[n]) e') e[1] ... e[n])
The reason that let is not a function is that it manipulates bindings. Why else might we want to have syntactic abstractions? Consider the following.
  (cond ((test then[1] ... then[n]) clause*))
= (if test (begin then[1] ... then[n]) (cond clause*)))

  (cond (else e[1] ... e[n]))
= (begin e[1] ... e[n])

  (cond) 
= undef
Here we see the second motivation for syntactic abstraction: to change argument evaluation order. Now lets look at and and or.
  (and e[0]) 
= e[0]

  (and e[0] ... e[n]) 
= (if (e[0]) (and (e[1] ... e[n]) #f))

  (or e[0])
= e[0]

  (or e[0] ... e[n])
= (let ((x e[0])) (if x x (or e[1] ... e[n])))     x not in FV(e[1] ... e[n])
Scheme has short-circuit evaluation like C. Suppose that we forget the condition that x not be in FV(e[1] ... e[n]).
(define x 1)
(or #f x)
This should be 1, but instead it is expanded into
(let ((x #f)) (if x x x))
What happened? variable x was captured by the binding introduced by the macro. We must have the hygiene condition that x not in FV(e[1] ... e[n]) to prevent this. We usually want hygiene for all variable bindings introduced by a macro. Hygienic macro systems do this automatically. The following shows that we have the same problem in C.
#define swap(x,y) { int temp = x; x = y; y = temp; }
...
swap(temp,x);

You can define macros in our Scheme system using extend-syntax. There you will find some examples of how to use this feature. Although the extend-syntax system we are using is not hygienic, we can get a little bit of help from gensym. It generates a unique symbol within the context of an extend-syntax expression. The following is an outline for the definition of or.

(extend-syntax (or)
  ((or e) e)
  ((or e1 e2 ... )
   (with ((x (gensym)))
	 (let ((x e1)
	       (if ... ))))))

Exercises

Reading