common-lispexpression-evaluationsetf

Evaluation of setf forms


This question is about the Common Lisp setf macro, and how it evaluates its argument forms (and subforms)--namely, only once if they happen to appear more than once. (It is also partly follow-up to an example given in the comments at Using get-setf-expansion.)

;create a list of two hash tables
* (defparameter hts (list (make-hash-table) (make-hash-table)))
HTS
* hts
(#<HASH-TABLE :TEST EQL :COUNT 0 {1007F76CB3}>
 #<HASH-TABLE :TEST EQL :COUNT 0 {1007F77103}>)

;define a function that swaps the position of the two hash tables
* (defun next-ht (hts) 
    (rotatef (first hts) (second hts))
    (second hts))
NEXT-HT

Swapping:

;now do a swap to verify it works
* (next-ht hts)
#<HASH-TABLE :TEST EQL :COUNT 0 {1007F76CB3}>
* hts
(#<HASH-TABLE :TEST EQL :COUNT 0 {1007F77103}>
 #<HASH-TABLE :TEST EQL :COUNT 0 {1007F76CB3}>)

;and swap them back
* (next-ht hts)
#<HASH-TABLE :TEST EQL :COUNT 0 {1007F77103}>
* hts
(#<HASH-TABLE :TEST EQL :COUNT 0 {1007F76CB3}>
 #<HASH-TABLE :TEST EQL :COUNT 0 {1007F77103}>)

Further testing:

;then set different values for a key in each table
* (setf (gethash 0 (first hts)) 11)
11
* (setf (gethash 0 (second hts)) 22)
22
* hts
(#<HASH-TABLE :TEST EQL :COUNT 1 {1007F76CB3}>
 #<HASH-TABLE :TEST EQL :COUNT 1 {1007F77103}>)

;finally execute a setf with a swapping side-effect
* (setf (gethash 0 (next-ht hts)) (1+ (gethash 0 (next-ht hts))))
23

;but it looks like hts has been swapped twice
;back to its original state
* hts
(#<HASH-TABLE :TEST EQL :COUNT 1 {1007F76CB3}>
 #<HASH-TABLE :TEST EQL :COUNT 1 {1007F77103}>)

;also, where did the initial value of 11 go?
* (gethash 0 (first hts))
23
T
* (gethash 0 (second hts))
22
T
*

Can someone clarify what's happening? Also, what is the meaning of a setf expression with a side-effect?


Solution

  • Why not macroexpand the setf form? Here LispWorks:

    CL-USER 32 > (pprint (macroexpand '(setf (gethash 0 (next-ht hts))
                                             (1+ (gethash 0 (next-ht hts))))))
    
    (LET* ((#:|key1014| 0)
           (#:|table1015| (NEXT-HT HTS))
           (#:|default1016| NIL)
           (#:|store1017| (1+ (GETHASH 0 (NEXT-HT HTS)))))
      (SYSTEM::%PUTHASH #:|key1014| #:|table1015| #:|store1017|))
    

    What does it do?

    So clearly NEXT-HT is called twice.

    What is the rough (!) conceptual model behind it?

    Example:

    CL-USER 62 > (setf (gethash (print 0)
                                (print (next-ht hts))
                                (print 1))
                       (print (1+ (print (gethash 0
                                                  (print (next-ht hts))
                                                  2)))))
    
    0 
    #<EQL Hash Table{1} 402000137B> 
    1 
    #<EQL Hash Table{0} 4020001573> 
    2 
    3 
    3   ; return value