On the other hand, although call-by-name is now dead in imperative languages (those employing effects, such as variable assignment, data structure mutation, and direct I/O), it is alive and well in purely functional languages, such as Haskell. Actually these languages typically use call-by-need, but given the purely functional context, this is just an optimization: there is no way to distinguish call-by-name and call-by-need, except by speed. (In the presence of effects, this isn't true.) In the functional programming community, some alternative nomenclature is frequently used: call-by-value is called "eager evaluation," call-by-name is called "lazy evaluation," and call-by-need is called "fully lazy evaluation."
Regarding the etymology of the term "thunk":
There are a couple of onomatopoeic myths circulating about the origin of this term. The most common is that it is the sound made by data hitting the stack; another holds that the sound is that of the data hitting an accumulator. Yet another suggests that it is the sound of the expression being unfrozen at argument-evaluation time. In fact, according to the inventors, it was coined after they realized (in the wee hours after hours of discussion) that the type of an argument in Algol-60 could be figured out in advance with a little compile-time thought, simplifying the evaluation machinery. In other words, it had "already been thought of"; thus it was christened a "thunk", which is "the past tense of `think' at two in the morning".(Eric Raymond, The New Hacker's Dictionary, The MIT Press, 1991, p. 350. See also the Jargon File on the web.)
On page 202 the Array Element type makes a reappearance from
Section 6.2, and just like there, we can eliminate it since we
have an Array ADT that supports the array-cell
operation. We can simply equate
The expression type local
is similar to let
except that it remains "call-by-value" even when let
is
changed to call-by-name. (Since let
is defined in terms
of procedure application, it follows the prevailing parameter passing
mechanism.) Incidentally, local
would also be handy with
Section 6.2's call-by-reference interpreter, for the same
reason. For a somewhat contrived example,
local x = 3 in let y = x in begin y := +(y, 1); *(x, y) endevaluates to 16 under call-by-reference or call-by-name, since
y
is an alias for or thunk for x
, so
y := +(y, 1)
increments x
. If we
instead write
local x = 3 in local y = x in begin y := +(y, 1); *(x, y) endthen we get 12 regardless of the parameter passing mechanism, because
y
is a new variable. (The outer local
could
be a let
in the case of call-by-reference or
call-by-need, without the values changing any. However, in the case
of call-by-name, the version with the outer and inner blocks both
being let
evaluates to 9, due to the subtlety described
in the next paragraph.)
The call-by-name interpreter described in EOPL does something very counter-intuitive if you assign to a formal parameter when the actual parameter wasn't a variable. For example, consider
let p = proc(x) begin x := 5; x end in p(3)This evaluates to 3, not 5. Each of the two places where
x
is used in the procedure body is replaced by a new cell
containing 3. The first cell is updated to 5, but the second cell
still contains 3. In Algol 60, this program would simply be
illegal: it isn't allowed to pass in a non-variable argument if the
formal parameter is assigned to in the procedure body. But under
EOPL's rules, this program is legal, just surprising.
Another surprising program would be the one shown in
Exercise 6.5.2 at the bottom of page 205. Notice that this
issue affects let
as well, since let
is just
a shorthand for procedure application. That leads to the surprising
value of 9 in the previous example of
let x = 3 in let y = x in begin y := +(y, 1); *(x, y) endAlso, note that this has a serious consequence for the implementation of
letrec
or letrecproc
. Expanding either
of these out into a let
of the names to some dummy
values and then in the body assigning the names new values won't
work. Instead, you will need to expand letrec
or
letrecproc
into a local
To understand the interpreter implementation in this section, it helps to know that only the indirect array model is considered.
For Figure 6.5.2, page 204, we can use the following replacements:
eval-rand
, we can use the indirect-array version I
gave in the Section 6.2 notes.
denoted->L-value
as shown in the
figure.
denoted->expressed
and
denoted-value-assign!
to
(define denoted->expressed (lambda (den-val) (cell-ref (denoted->L-value den-val)))) (define denoted-value-assign! (lambda (den-val exp-val) (cell-set! (denoted->L-value den-val) exp-val)))
Exercise 6.5.4, page 206, introduces Jensen's device in the context of approximating definite integrals, but it can be simplified to just summing a function over a range, e.g., the sum for k from 1 to 10 of k2. In Scheme we would write
(define from-to-sum (lambda (low high f) (if (> low high) 0 (+ (f low) (from-to-sum (+ low 1) high f))))) (from-to-sum 1 10 (lambda (k) (* k k)))We could translate this directly into Ted, using
proc
in
place of lambda
, getting
fromToSum(1, 10, proc(k) *(k, k))But Jensen's device tells us that in the call-by-name variant of Ted (or, originally, in Algol 60), we can dispense with the
proc
and write
forFromToSum(k, 1, 10, *(k, k))with a suitable definition of
forFromToSum
, which
repeatedly evaluates its fourth argument (here
*(k, k)
), with its first argument (here
k
) set to each value in the range specified by the second
and third arguments. As I mentioned earlier, this is a historical curiosity.
Finally, note that Section 6.5's discussion of call-by-name
and call-by-need assumes that when any primitive procedure is applied,
the argument denoted values (thunks or memos) are first converted to
expressed values. (This is done in apply-proc
, which
still remains unchanged from Figure 6.1.2, page 182.) This
policy is known as having strict primitive procedures.
We can usefully change cons
to be non-strict, so that
it builds a pair of denoted values (thunks or memos) instead of a pair
of expressed values. We can then change car
and
cdr
to convert the denoted values to expressed values
when they are accessed. (This particularly is viable, from an
efficiency standpoint, if call-by-need memos are used rather than
call-by-name thunks.)
By making this simple change, it becomes possible to make infinitely large data structures, so long as we only ever look at finitely much of them. For example, the following expression make an infinitely long list of all the positive integers, and then selects the third element of that list (3).
letrec intsFrom = proc(low) cons(low, intsFrom(+(low, 1))) in local positiveIntegers = intsFrom(1) in car(cdr(cdr(positiveIntegers)))This would make a fun project, if you are looking for an extra challenge.
Instructor: Max Hailperin