Search code examples
variablesreferencecommon-lispvariable-assignmentlet

Why does "setf" not work when using "let"?


The behavior of setf when combined with let is confusing to me. Here, setf isn't changing the second value of my list.

(defun to-temp-field (lst)
  (let ((old-value (nth 2 lst))
        (new-value 'hello))
    (progn
      (setf old-value new-value)
      lst)))

But if I do it without let, it changes ok:

(defun to-temp-field (lst)
  (let ((new-value 'hello))
    (progn
      (setf (nth 2 lst) new-value)
      lst)))

What's causing this behavior?


Solution

  • setf takes a place and a value, or a number of place/value pairs, and mutates the value of the place to the new value.

    In the first code example old-value is a lexical variable bound within the let form. Lexical variables are places, so calling (setf old-value new-value) sets the value of old-value to new-value.

    Function forms can also be places, and nth is one function that can specify a place. In the second code example calling (setf (nth 2 lst) new-value) sets the value of the place (nth 2 lst) to the value new-value.

    So in the first example the place old-value, a lexical variable, is updated. But in the second example the form (nth 2 lst) is a place, and the object which this form evaluates to (an element of the list lst) is updated.

    To be clear: a place is not a value, it is a form.

    With (let ((old-value (nth 2 lst)) ;;...)) old-value is bound to the value to which the form (nth 2 lst) evaluates. But with (setf (nth 2 lst) new-value) the form (nth 2 lst) is interpreted as a place. (setf old-value new-value) works the same way: old-value is a form, and a place. Here the variable old-value has been let-bound to the value of the form (nth 2 lst), and it is this value which is replaced via assignment in the call to setf by mutating the binding of old-value. As a result the value of the place old-value has been changed.

    A few things to note:

    • The second example updates the third element of lst since lists are zero-indexed in Common Lisp.

    • There is no need for the progn form inside of let since the body of a let form is an implicit progn.

    • Attempting to modify a literal causes undefined behavior in Common Lisp, so you shouldn't call the second function with a quoted list like this: (to-temp-field '(1 2 3)) or with a variable bound to a list literal, but rather like this: (to-temp-field (list 1 2 3)) or with a variable bound to a list that has been otherwise constructed.