1

I want to write a function in Common Lisp, which destructively modifies its argument. In C or Rust I would use a pointer/reference to an object, which can be dereferenced inside the body of the function. In CL I write:

(defun foo (lst)
  (setf lst NIL))

But after evaluating this form:

(let ((obj (list "a" "b")))
  (foo obj)
  obj) => ("a" "b")

one sees that the function foo has no effect. I can explain that by pass by value semantic, where we modify a local copy of the argument pushed onto the function stack. If we define another function:

(defun bar (lst)
  (setf (car lst) NIL))

and evaluate a similar form

(let ((obj (list "a" "b")))
  (bar obj)
  obj) => (NIL "b")

we will clearly see that the lst was modified as if we would use the pass by reference semantic. So, (setf lst NIL) did not worked but (setf (car lst) NIL) did. Could you please explain why?

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
IgSokolov
  • 55
  • 4
  • The problem is with `setf`. It's first argument requires a `place`. In `foo` the symbol `lst` goes out of context, but in `bar` the place still exists in outer context. – rajashekar Nov 01 '21 at 13:01
  • 2
    `setf` with a symbol as place means you want to mutate what the variable is pointing at, not the object it is bound to. Eg. `lst = 0` is the same as `(setf lst nil)`. `lst` is an object so `(setf (car lst) ...)` mutates the car accessor of the object, thus yo uare now updating the object and not the binding. – Sylwester Nov 01 '21 at 13:08

2 Answers2

5

Common Lisp passes arguments by value, but in most cases (other than primitive value types like fixnums or floats) the value is a reference. This is the same way that most managed languages work (e.g. Java, Python, JS).

In the LET-form, the value of the variable OBJ is a reference to a list, not the list itself. That reference is passed by value, so that inside the functions, the value of the parameter LST is another reference to the same list.

In FOO, the reference value is replaced with NIL, but the list that was previously referred to is not touched at all. In BAR, the list is retrieved from the heap and its CAR is replaced with NIL. Since OBJ holds a reference to the same list, the modification affects it as well.

jkiiski
  • 8,206
  • 2
  • 28
  • 44
  • Thank you for the answers. One thing remained not so clear. Can I decide myself, whether `setf` should follow a pointer, retrieve an object from the heap and modify it, OR it should just modify its binding? How to modify the function `foo` so that it does set its argument to NIL? – IgSokolov Nov 01 '21 at 20:53
  • If you use a plain variable name as the place for `SETF`, it will always only modify the variable, not the object being referred to. To modify the object, you have to use some accessor (such as `CAR`, `CDR`, `AREF`, etc.). To get `FOO` to work like you want, you have to wrap the list in some kind of box object that you can then modify (e.g. a zero-rank array or a struct), but it's more idiomatic Lisp to just return the modified object as per [Rainer Joswig](https://stackoverflow.com/a/69801241/5747548). – jkiiski Nov 02 '21 at 04:48
3

For some operations the usual style is this:

(let ((obj (list "a" "b")))
  (setf obj (nbar obj))  ; nbar is destructively modifying the list argument
  (foo obj))

Above uses SETF to use the return value to change the binding of obj, since the function nbar itself can't do it.

Since NBAR has no access to the lexical variable obj and thus can't change it, one is supposed to return the object from NBAR and then one sets the variable to the result value - overwriting the previous binding.

Examples, where this rule is applicable: calls to sort, nreverse, ...

Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346