• No results found

54 [ ] Most languages support arrays, in which case array references are generally treated like variable references under call-by-reference That is, if an operand is an array reference, the location referred to,

Further Reading

Exercise 3. 54 [ ] Most languages support arrays, in which case array references are generally treated like variable references under call-by-reference That is, if an operand is an array reference, the location referred to,

rather than its contents, is passed to the called procedure. This allows, for example, a swap procedure to be used in commonly occurring situations in which the values in two array elements are to be exchanged. Add array primitives like those of exercise 3.42 to the call-by-reference language of this section, and extend eval-rand to handle this case, so that, for example, a procedure application like (swap

(arrayref a i) (arrayref a j)) will work as expected.

Exercise 3.55 [ ] Call-by-value-result is a variation on call-by-reference. In call-by-value-result, the actual parameter must be a variable. When a parameter is passed, the formal parameter is bound to a new reference initialized to the value of the actual parameter, just as in call-by-value. The procedure body is then executed normally. When the procedure body returns, however, the value in the new reference is copied back into the reference denoted by the actual parameter. This may be more efficient than call-by-reference because it can improve memory locality. Implement call-by-value-result and test it with a program that produces different answers using call-by-value-result and call-by-reference.

We now turn to a very different form of parameter passing, called lazy evaluation. Sometimes in a given call a procedure never refers to one or more of its formal parameters. In this case time devoted to evaluating the corresponding operands is wasted. It may even be that evaluation of such an operand would result in an error or never terminate. For example, were it not for such problems, if could be a procedure, instead of having to be a syntactic form.

In a language such as Scheme that supports first-class procedures, one can delay (perhaps indefinitely) the evaluation of an operand by encapsulating it as the body of a thunk, a procedure of no arguments. Whenever a variable is referenced, the corresponding procedure must be invoked. The actions of forming thunks and evaluating them are called freezing and thawing, respectively.

A few languages support a parameter-passing mechanism called lazy evaluation that automates this technique. Lazy evaluation mechanisms may differ in how they handle multiple references to the same parameter. A naive approach would invoke the thunk every time the parameter is referred to. This policy is called, for historical reasons, call-by-name. In the absence of side effects this is a waste of time, since the same value is returned each time. A more sophisticated approach, called call-by-need, records the value of each thunk the first time it is invoked, and thereafter refers to the saved value instead of re-invoking the thunk. This is an example of a more general technique known as memoization.

In the absence of side-effects, call-by-name and call-by-need always give the same answer. In the presence of side-effects, however, it is easy to distinguish these two mechanisms. Consider, for example, the expression

let g = let count = 0 in proc () begin set count = add1 (count); count endin (proc (x) +(x,x) (g))

The procedure g returns the number of times it is called. Under call-by-name each reference to the variable x

invokes g, so the first x evaluates to 1, the second x evaluates to 2, and the result is 3. Under call-by-need, g is invoked only once, for the first reference to x, so both occurrences of x evaluate to 1, and the result is 2.

An attraction of lazy evaluation in all its forms is that in the absence of side-effects it supports reasoning about programs in a particularly simple way. The effect of a procedure call can be modeled by replacing the call with the body of the procedure, with every reference to a formal parameter in the body replaced by the corresponding operand. This evaluation strategy is the basis for the lambda calculus, in which it is referred to as β-reduction. (See exercise 2.12.) In other languages it is sometimes called the copy rule.

Even with call-by-need there can be considerable overhead associated with so much freezing and thawing activity. It is, however, possible to reduce this overhead to often-acceptable levels, primarily by not making thunks when it can be proved that the result will not be changed.

A more important reason why call-by-name is not popular is that it generally makes it difficult to determine the flow of control (order of evaluation), which in turn is essential to understanding a program with side effects. On the other hand, if there are no side effects, the flow of control does not affect the result of a program, so this is not a problem. Thus lazy evaluation is popular in purely-functional programming languages (those with no side-effects), and rarely found elsewhere. We now add lazy evaluation to our language. As before, variables denote references to expressed values:

We implement lazy evaluation by extending our data type of references to add a third kind of target, called a thunk target. A thunk target is like a direct target, except that instead of containing an expressed value it contains a thunk that evaluates to an expressed value. If deref is given a reference containing a thunk (either as a direct or indirect target), it evaluates the thunk using

eval-thunk, which evaluates the expression contained in the thunk and returns the

corresponding expressed value; further, if the system is using call-by-need, eval-thunk updates the location containing the thunk to contain instead a direct target with the expressed value. See figures 3.19 and 3.20.

In eval-rand we recognize literals and procedures and do not bother to freeze them, since they

evaluate quickly. We also give special treatment to operands that are variables, as in call-by- reference and we treat thunk targets in the same way that we treat direct targets. Last and most important, all other operands are frozen by creating a thunk that delays their evaluation until needed (figure 3.21). Thus, under call-by-need, in the expression

Figure 3.19 Implementation of references for call-by-name and call-by-need (part 1)

(proc (a, b) (proc (x) (proc (y) (proc (z) + (+(x,y), z) y) x) +(a, b))15 20)

the operand + (a, b) gets evaluated only when the first variable is referenced in + (+ (x,

y),z), regardless of which variable is evaluated first, and it is evaluated only once. Each of the

other two variables refers to the same already-evaluated thunk.

Exercise 3.56 [ ] Implement the call-by-need interpreter, but leave if out of the language syntax and