The Importance of Terminology's Quality

Robert Maas, jaycx2.3.calrobert at spamgourmet.com.remove
Sun Jul 20 13:36:21 EDT 2008


> >> ... the "thunks" were necessary at the machine-language level to
> >> /implement/ ALGOL 60, but they could not be expressed /in/ ALGOL.
> > Ah, thanks for the clarification. Is that info in the appropriate
> > WikiPedia page? If not, maybe you would edit it in?
> From: John W Kennedy <jwke... at attglobal.net>
> It is explained s.v. "thunk", which is referenced from "ALGOL
> 60". The ALGOL "pass-by-name" argument/parameter matching was
> perhaps the most extreme example ever of a language feature that
> was "elegant" but insane. What it meant, in effect, was that,
> unless otherwise marked, every argument was passed as two closures,
> one that returned a fresh evaluation of the expression given as the
> argument, which was called every time the parameter was read, and
> one that set the argument to a new value, which was called every
> time the parameter was set.

Wow! All these years when I occasionally heard of a "thunk" I never
was told, until now, what it really meant. Thanks for the info!!

Followup question #1: I assume these are lexical closures in the
environment of the point of the call, right?

Followup question #2: For simple arithmetic expressions, I can
possibly understand how the UPDATE closure might be implemeted
(expressed in Lisp to make the intent clear):
  Call form:  MyFunction(X+2);
  GET closure:  (+ closedX 2)
  UPDATE closure:  (lambda (newval) (setf closedX (- newval 2))
Thus from inside MyFunction where formal parameter Arg1 is bound
to actual parameter X+2, after doing Arg1 := 7; X will have the
value 5 so that calling Arg1 will return 7 as expected, right?
But if the actual argument is something complicated, especially if
it makes a nested function call, how can that possibly be
implemented? Given an arbitrary expression that calls some external
function, how can assigning a value to that expression make
sufficient changes in the runtime environment such that
subsequently evaluating that expression will yield the expected
value i.e. the value that had been assigned?
Or is the default of passing two closures (GET and UPDATE) *only*
if the actual-argument expression is simple enough that it's
invertible, and in complicated cases only a GET closure is passed
(or the UPDATE closure is simply a signal of a runtime error
 that you're not allowed to assign a value to a complicated expression)?

IMO the "right" way to pass parameters that can be modified is to
use "locatatives" as in the Lisp Machine. That converts the idea of
a "place" (as used by SETF in Common Lisp) into a "first class
citizen" which can be passed around and stored etc., compared to a
SETF place which is merely a compiletime-macro trick to convert
place-references in source code into direct calls to the
appropriate accessor just above the place followed by specialized
SETter call to do the act. A hack to emulate a locative in CL would
be to pass a closure where the code to find the object directly
containing the place, and any parameters needed to find that place,
and the function needed to perform the act. Then the called
function would need to know it's going to get such a thunk-like
closure, but since it's expecting to modify one of its parameters
anyway, that's reasonable. Sketch of implementation (two special cases):
(defun make-thunk-cadr (topptr)
  (let* ((midptr (cdr topptr))
         (getclo (make-getter-closure :PARENT midptr :GETTERFN #'car
                                      :PARMS nil))
         (setclo (make-setter-closure :PARENT midptr :SETTERFN #'rplaca
                                      :PARMS nil)))
   (make-thunk getclo setclo))
(defun make-thunk-aref1 (topptr arrindex1)
  (let ((getclo (make-getter-closure :PARENT topptr :GETTERFN #'aref1
                                     :PARMS (list arrindex1)))
        (setclo (make-setter-closure :PARENT midptr :SETTERFN #'setaref1
                                     :PARMS (list arrindex1))))
   (make-thunk getclo setclo))
(defun swap (thunk1 thunk2)
  (prog (tmp)
    (setq tmp (thunk-get thunk1))
    (thunk-set thunk1 (thunk-get thunk2))
    (thunk-set thunk2 tmp)))
;Definitions of make-getter-closure make-setter-closure make-thunk
; thunk-get thunk-set not shown because they depend on whether
; closures and thunks are implemented via tagged assoc lists or
; DEFSTRUCT structures or CLOS objects or whatever. But I made the
; call to the constructors explicit enough that it should be obvious
; what components are inside each type of object. Note that with
; CLOS objects, this could all be condensed to have a single CLOS
; object which is the thunk which has two methods GET and SET, no
; need to make closures for get and set separately, templates for
; those closures are made automatically when the CLOS class is
; defined, and closures are generated from those templates whenever
; a new CLOS thunk-object is made. Thus:
; ... (make-CLOS-thunk :PARENT topptr :GETTERFN #'aref1 :SETTERFN #'setaref1
;                      :PARMS (list arrindex1)) ...

;Example that should actually work:
(format t " arr: ~S~% ixs: ~S~%" arr ixs)
 arr: #'(3 5 7 11 13))
 ixs: (2 4)
(setq arr #'(3 5 7 11 13))
(setq ixs (list 2 4))
(setq thunkcadr (make-thunk-cadr ixs))
  ;Locative to the 4 in ixs
(setq thunkaref (make-thunk-aref1 arr (thunk-get thunkcadr)))
  ;Locative to the 11 in the array
(swap thunkcadr thunkaref)
(format t " arr: ~S~% ixs: ~S~%" arr ixs)
 arr: #'(3 5 7 4 13))
 ixs: (2 11)
I haven't implemented this. I'm just specifying what the behaviour should be
and giving a sketch how it ought to be easily doable in Common Lisp.

And I'm not going to implement it because I have no use for this way
of coding, at least not currently or in the foreseeable future.
<tmi> Generally my abstract data type is at a higher level where the
      caller doesn't know that a single place is going to need to be
      SETFed, so there's no point in getting a locative to work with.
      Instead there's some *kind* of update to do, and parameters to
      that *kind* of update; what really happens internally (one or more
      SETFs, or alternately re-build anything that changed and share what
      didn't change) doesn't need to be known by the caller. All the
      caller needs to know is generically whether the update is in-place
      or non-destructive. (If it's non-destructive, then the new edition
      of the data structure is one of the return values. If it's
      in-place, then there's no need to bother with setq of the new
      value, because my structures always have a header cell that has a
      tag for the intentional datatype, and that header cell always
      points to either the in-place-modified object or the
      latest-edition-of-object.) </tmi>
<mtmi> I'd rather program in "paranoid" mode than in "risk shoot foot" mode.
       The CAR of each ADT object is a keyword identifying the
       intentional type of that object, and every function that
       operates on that intentional type first checks if the parameter
       really does have the expected CAR, just to make sure I didn't
       copy&paste some inappropriate function name in my code. Yeah, it
       takes extra CPU cycles to do that checking on every calls, but it
       sure saves me from shooting myself in the foot and having to spend
       an hour to find out how I did it before I can fix it. <mtmi>
<emtmi> TMI = Too Much Information (actually YMMV, some readers might like it)
        MTMI = More of Too Much Information
        EMTMI = Even More of Too Much Information (only newbies need read)
        Credits to Babylon Five for the "I spy" game in the cargo hold.
        I spy something that starts with the letter B.   Boxes!
        I spy something that starts with the letter M.   More boxes! <emtmi>



More information about the Python-list mailing list