(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) ... )))
(define memo
(lambda (f)
(let ((first-time #t)
(value #f))
(lambda ()
(if first-time
(begin (set! value (f)) (set! first-time #f))
value)
value))))
...
(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.
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.
(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