• No results found

Further Reading

Exercise 3. 48 [ ] Our understanding of assignment, as expressed in the interpreter of figure 3.15, depends on the semantics of side effects in Scheme In particular, it depends on when these effects take place If we

3.8 Parameter-Passing Variations

The language design of section 3.7, in which formal parameters are bound to locations of operand values, has used call-by-value. This is the most commonly used form of parameter passing, and is the standard against which

(define-datatype answer answer? (an-

answer (val expval?) (store store?)))(define eval-

expression (lambda (exp env store) (cases expression exp (var- exp (id) (an-answer (apply-store store (apply-

env env id)) store)) (varassign-exp (id rhs-

exp) (cases answer (eval-expression rhs-exp env store) (an- answer (val new-store) (an-answer 1 (extend-

store (apply-env env id) val store))))) (if-exp (test-exp true- exp false-exp) (cases answer (eval-expression test-

exp env store) (an-answer (val new-store) (if (true- value? val) (eval-expression true-exp env new-

store) (eval-expression false-exp env new- store))))) ...)))

Figure 3.16 Store-passing interpreter for exercise 3.48

other parameter-passing mechanisms are usually compared. In this section we explore alternative parameter-passing mechanisms.

Consider the following expression:

let a = 3 p = proc (x) set x = 4in begin (p a); a end

Under call-by-value semantics, the denoted value associated with x is a reference that initially contains the same value as the reference associated with a, but these references are distinct. Thus the assignment to x has no effect on the contents of a's reference, so the value of the entire expression is 3.

With call-by-value semantics it is a big help to know that when a procedure assigns a new value to one of its parameters, this cannot possibly be seen by its caller. Of course, if the parameter passed to the caller contains a

reference to a mutable location, as in exercise 3.42, and the procedure modifies this location, the resulting modification will still be seen by the caller in subsequent uses of the reference. Though this isolation between the caller and callee is generally desirable, there are times when it is valuable to allow a procedure to be passed variables with the expectation that they will be assigned by the procedure. This may be accomplished by passing the procedure a reference to the location of the caller's variable, rather than the contents of the variable. This parameter-passing mechanism is called call-by-reference. If an operand is simply a variable reference, a reference to the variable's location is passed. The formal parameter of the procedure is then bound to this location. If the operand is some other kind of expression, then the formal parameter is bound to a new location containing the value of the operand, just as in call-by-value. Using call-by-reference in the above example, the assignment of 4 to x has the effect of assigning 4 to a, so the entire expression would return 4, not 3.

One common use of call-by-reference is to return multiple values. A procedure can return one value in the normal way and assign others to parameters that are passed by reference. For another sort of example, consider the common programming need for swapping the values in two variables:

let a = 3 b = 4 swap = proc (x,

y) let temp = x in begin set x = y; set y = temp endin begin (swap a b); - (a,b) end

Under call-by-reference, this swaps the values of a and b, so it returns 1. If this program were run with our existing call-by-value interpreter, however, it would return -1, because the assignments inside the swap procedure then have no effect on variables a and b.

The only change occurs when new references are created. Under call-by-value, a new reference is created for every evaluation of an operand; under call-by-reference, a new reference is created for every evaluation of an operand other than a variable.

Because call-by-value creates a new location for every operand in a procedure application, we could put the values of all the operands in a vector, and have apply-env-ref create a reference to the location at variable-lookup time. Under call-by-reference, however, we will need a new location for some operands and not for others, so we need a different representation for references. For our implementation of call-by-reference, we will use the implementation of references shown in figure 3.17. A reference will be, as before, a reference to a location within a vector. But the vector, instead of containing expressed values, will contain either expressed values or references to expressed values. We call these two kinds of targets direct targets and indirect targets,

respectively. A direct target corresponds to the behavior of call-by-value, in which a new location is created; an indirect target corresponds to the new behavior of call-by-reference, in which no new location is created. The new definitions of deref and setref! look at the kind of target to determine the expressed value to return or the location to mutate.

The procedures extend-env and apply-env-ref are unchanged: extend-env will take a list of targets and return a vector containing those targets, and apply-env-ref looks up an identifier and creates a reference to the location containing the appropriate target.

Now we can implement call-by-reference. We consider each place where subexpressions are evaluated. For primitive applications, we simply need to evaluate the subexpressions and pass the values to apply-primitive, so in eval-expression we write

(primapp-exp (prim rands) (let ((args (eval-primapp-exp- rands rands env))) (apply-primitive prim args)))

where eval-primapp-exp-rands is defined by

(define eval-primapp-exp-

rands (lambda (rands env) (map (lambda (x) (eval- expression x env)) rands)))

For let-bound variables, we choose to retain the call-by-value behavior, so in eval-

(define-datatype target target? (direct-target (expval expval?)) (indirect- target (ref ref-to-direct-target?)))

(define expval? (lambda (x) (or (number? x) (procval? x))))(define ref-to- direct-

target? (lambda (x) (and (reference? x) (cases reference x (a- ref (pos vec) (cases target (vector-ref vec pos) (direct-

target (v) #t) (indirect-target (v) #f)))))))

(define deref (lambda (ref) (cases target (primitive-deref ref) (direct- target (expval) expval) (indirect-

target (ref1) (cases target (primitive-deref ref1) (direct- target (expval) expval) (indirect-target (p) (eopl: error 'deref "Illegal reference: ~s" ref1)))))))

(define setref! (lambda (ref expval) (let ((ref (cases target (primitive-

deref ref) (direct-target (expval1) ref) (indirect- target (ref1) ref1)))) (primitive-setref! ref (direct-target expval)))))

(let-exp (ids rands body) (let ((args (eval-let-exp- rands rands env))) (eval-expression body (extend-

env ids args env))))

where eval-let-exp-rands and eval-let-exp-rand are defined by

(define eval-let-exp-rands (lambda (rands env) (map (lambda (x) (eval- let-exp-rand x env)) rands)))(define eval-let-exp-

rand (lambda (rand env) (direct-target (eval-expression rand env))))

For procedure applications, we continue to evaluate each operand using eval-rand.

(define eval-rand (lambda (rand env) (cases expression rand (var- exp (id) (indirect-target (let ((ref (apply-env-

ref env id))) (cases target (primitive- deref ref) (direct-

target (expval) ref) (indirect-

target (ref1) ref1))))) (else (direct-target (eval- expression rand env))))))

Here we must be a bit more careful. If the operand is a non-variable, then we create a new location, as before, by returning a direct target. If the operand is a variable, it denotes a location containing an expressed value, so we want to return an indirect target pointing to that location. This is a bit trickier than it first appears. If a variable is bound to a location containing a direct target (which must contain an expressed value, like 5), then a reference to the location is returned as an indirect target. But, if the variable is bound to another reference, then that reference is returned. This maintains the invariant that a reference contains either an expressed value or a reference to an expressed value.

We show the operation of eval-rand in figure 3.18 where we depict the value ribs in the environment of the innermost procedure body in the program

Figure 3.18 Environments built by call-by-reference

(proc (t, u, v, w) % call this p1 (proc (a, b) % call this p2 (proc (x, y, z) % call this p3 set y = 13 a b 6) 3 v) 5 6 7 8)

First the procedure p1 is applied to 5, 6, 7, and 8, yielding the value vector at the bottom of the figure. Next p2 is applied to the operands 3 and v, yielding the value vector in the middle. This vector contains 3 and a reference to the location containing 7. In each vector element, there is a direct-target wrapped around each expressed value and an indirect-target wrapped around each reference; these are not depicted to preserve the clarity of the picture. Finally, p3 is invoked on a, b, and 6. The variable a contains a direct target, so x is bound to an indirect target containing a pointer to a. The variable b contains an indirect target, so y is bound to an indirect target containing a pointer to the target of b. Last, 6 is an expressed value, so z is bound to a direct target containing 6.