COS 441 - Parameter Passing Too - Feb 29, 1996


Suppose we encounter a call (f arg1 arg2 arg3) in which the argument values are expensive to compute and don't have side effects. If f is defined as
(define f (lambda (x y z) 1))
Then we need not bother to compute the argument values. If we delay the evaluation of an argument until its value is needed we have a new parameter passing method, call-by-name. Algol is an example of a language that used this calling convention. The relevant clauses for a call-by-name interpreter follow.
     (Var (name) ((unbox (lookup env name))))
     (Ap (fun arg)
       (variant-case (eval fun env)
	  (Closure (formal body env) 
	    (eval body (extend env formal (lambda () (eval arg env)))))))
Prefixing lambda () to an expression is called a thunk. Invoking the function created by a thunk is referred to as thawing the thunk. Call-by-name is difficult to reason about because of the delayed evaluation of arguments. Assignment compounds this problem. Consider:
(let ((a 0)
      (b (array 1 0))
      (swap (lambda (x y) (let ((temp x)) (set! x y) (set! y temp)))))
  (swap a (array-ref b a)))
We can achieve the effect of call-by-name in a call-by-value language by using thunks in the code, rather than the interpreter.
(f arg1 arg2 arg3) -> (f (lambda () arg1) (lambda () arg2) (lambda () (arg3)))

(define f
  (lambda (x y z)
    (... (x) ... (x) ... (y) ... (z) ... )))


A downside of call-by-name is that each time a variable is used, it is re-evaluated. This leads to yet another parameter passing method, call-by-need. Here we "memoize" or "cache" result of each thunk.
(define memo
  (lambda (f)
    (let ((first-time #t)
	  (value #f))
      (lambda ()
	(if first-time
            (begin (set! value (f)) (set! first-time #f))

     (Ap (fun arg) 
       (variant-case (eval fun arg)
	 (Closure (formal body env)
	   (eval body (extend env formal (memo (lambda () (eval arg env))))))))
Call-by-need is the basis of several so called ``lazy'' languages, like Haskell and Miranda. Since it is even more difficult to reason about call-by-need in the presence of assignment, call-by-need tends to be used only in languages that do not have assignment.

So what good are call-by-name and call-by-need? Languages with these parameter passing methods (and without assignment) provide a powerful reasoning principle called beta-reduction:

((lambda (x) e1) e2) = e1[x -> e2]
Here e1[x -> e2] denotes the substitution of e2 for free occurrences of x in e1, avoiding capture. Avoiding capture means we must ensure that BV(e1) does not intersect FV(e2). This condition doesn't prevent us from using applying beta, as we can always just change the names of the bound variables of e1. (By the way, the rule that lets us change names is called alpha.) The beta rule does not hold in Scheme. To see this, let Omega be an infinite loop and consider:
((lambda (x) 1) Omega) != (1 [x -> Omega] = 1)
In Scheme we have a weaker rule called beta-value:
((lambda (x) e) v) = e[x -> v] where v is a constant or lambda.
We can get most of the benefit of call-by-need in Scheme using delay and force. These procedures return a "promise" and evaluate the "promise" respectively.


Continuation Passing Style

Consider the procedure Pi which computes the product of a list.
(define Pi
  (lambda (l)
    (cond ((null? l) 1)
	  ((zero? (car l)) 0)
	  (else (* (car l) (Pi (cdr l)))))))
This implementation is certainly correct, but it performs all the multiplications preceding the first zero in the list. Let's see if an accumulator will solve the problem.
(define Pi (lambda (l) (Pi-Prime l 1)))

(define Pi-Prime 
  (lambda (l acc)
    (cond ((null? l) acc)
	  ((zero? (car l)) 0)
	  (else (Pi-Prime (cdr l) (* (car l) acc))))))
This has the same problem as the previous attempt. But we're close. Let's add some thunks.
(define Pi (lambda (l) (Pi-Prime l (lambda () 1))))

(define Pi-Prime 
  (lambda (l acc)
    (cond ((null? l) (acc))
	  ((zero? (car l)) 0)
	  (else (Pi-Prime (cdr l) (lambda () (* (car l) (acc))))))))
This works. Now all the multiplications are delayed in thunks until every element of the list has been examined. The thunks start being thawed only when we hit the end of the list (see the null? test).

This is nice, but the base case is not a part of Pi-Prime. To move it in, we add an argument to the thunks:

(define Pi (lambda (l) (Pi-Prime l (lambda (x) x))))

(define Pi-Prime 
  (lambda (l acc)
    (cond ((null? l) (acc 1))
	  ((zero? (car l)) 0)
	  (else (Pi-Prime (cdr l) (lambda (x) (* (car l) (acc x))))))))
Now we've got a function very much like the original, except it does less work if there is a zero in the list. Unfortunately, it also does the multiplies from left to right, where the original function did them from right to left. To make this function more like the original, we change the order in which the multiply and the invocation of acc occur in the last line:
(define Pi (lambda (l) (Pi-Prime l (lambda (x) x))))

(define Pi-Prime 
  (lambda (l acc)
    (cond ((null? l) (acc 1))
	  ((zero? (car l)) 0)
	  (else (Pi-Prime (cdr l) (lambda (x) (acc (* (car l) x))))))))
This final modification gives us the property that there is nothing to do after to recursive call to Pi-Prime. When a procedure satisfies this property, it is said to be in continuation-passing-style.

To CPS-convert a procedure f to f-prime, we do the following.

Let's try this on a procedure that computes the factorial of a number.
(define fact
  (lambda (n)
    (if (zero? n) 1 (* n (fact (sub1 n))))))

(define fact-prime
  (lambda (n k)
    (if (zero? n) (k 1)
	(fact-prime (sub1 n) (lambda (x) (k (* n x)))))))
Another more complicated example is finding the maximum of set.
(define max
  (lambda (tree)
    (if (number? tree) tree
	(if (>= (max (car tree) (max (cdr tree)))) (max (car tree))
	    (max (cdr tree))))))

(define max-prime
  (lambda (tree k)
    (if (number? tree) (k tree)
	(max-prime (cdr tree) (lambda (x)
	  (max-prime (car tree) (lambda (y) 
	    (if (>= y x) (max-prime (car tree) (lambda (x) (k x)))
		         (max-prime (cdr tree) (lambda (x) (k x)))))))))))
Note: (lambda (x) (k x)) => k (this rule is called eta. This can be used to simplify max-prime