Why I can't (push 3 '()) in Common Lisp's REPL?

Viewed 127

I am using Common Lisp and Slime inside Emacs. I have more experience avoiding mutation than using it.

In the REPL I can do:

CL-USER> (defvar so-example '())
SO-EXAMPLE

CL-USER> (push 7 so-example)
(7)

And it works. However, if I try:

CL-USER> (push 7 '())

I get an error message saying:

Undefined function: (SETF QUOTE)

OK. Since quote was a problem, I also tried:

CL-USER> (push 7 nil)

Which also throws an error message:

NIL is a constant and thus can't be set.

Why does this happen? Why it makes sense?

For me, this is strange.

3 Answers

Modification of literal constants results in undefined behavior in Common Lisp; from the HyperSpec:

The consequences are undefined if literal objects (including quoted objects) are destructively modified.

This explains the error message observed with (push 7 nil); the same error message would accompany (push 7 ()).

The expression (push 7 '()) is equivalent to (setf '() (cons 7 '())). setf expects a place, but (quote ()) is not a place. The setf macro is catching the mistake when it realizes that quote cannot produce a settable place.

Note that according to the documentation, push expects an item and a place (not a list): "push prepends item to the list that is stored in place....". That is, the place is a reference to a list, not the list itself.

Generally speaking, it does not make much sense to attempt to mutate a literal constant like this. Consider doing this with numbers, instead. Attempting to change 7 to 8 by modifying the number 7 is not what we would attempt to do. Instead we would establish a binding to 7, and mutate the binding. Doing the same thing with an empty list:

CL-USER> (defvar empty-list '())
SO-EXAMPLE
CL-USER> (setf empty-list (cons 7 empty-list))
(7)

Here a binding to the empty list is established (empty-list), then the binding is mutated, binding empty-list to a fresh list created by consing 7 to (). This is exactly what happened in the first posted example which worked.

CL-USER> (defvar so-example '())
SO-EXAMPLE
CL-USER> (push 7 so-example)
(7)

A similar example using arithmetic might be:

CL-USER> (defvar x 7)
X
CL-USER> (setf x (+ 1 x))
8
CL-USER> x
8

Yet you probably would not expect (setf 7 (+ 1 7)) to do anything good.

The documentation for the macro PUSH says:

push item place => new-place-value

Places are generalized references: variables, array slots, structure slots, CLOS object slots, and more. They are documented here: CLHS 5.1 Generalized Reference

What does generalized mean? In Common Lisp places are a concept which go beyond simple references like variables or slots. It is even user extensible -> one can defined new types of places.

NIL as a place

NIL (same as ()) is documented as a Constant Variable.

Thus something like this fails:

(setf nil 10)

(push 10 nil) is trying to push 10 to the place NIL.

CL-USER 1 > (macroexpand-1 '(push 10 nil))
(LET ((#:|new-value-1070| 10))
  (LET* ((#:|Store-Var-1069| (CONS #:|new-value-1070| NIL)))
    (SETQ NIL #:|Store-Var-1069|)))

There were have it: one tries to set NIL (as a reference, here a variable) to the value 10.

Since NIL is defined as a constant variable and constant variables are not to be changed, one can't push something to the place nil.

Thus NIL is not a useful place.

'NIL as a place

Why does (push 10 '()) fail? Which is the same as (push 10 'nil) and (push 10 (quote nil)).

Again, push expects a place. (quote ...) is not a defined place.

The default defined places are defined here: CLHS 5.1.2 Kinds of Places.

Which makes sense, since '() does not reference anything. Remember: places are generalized references. It is supposed to be a literal object ().

Function CONS

If we want to add a new object to a list, then we use cons:

CL-USER 4 > (cons 10 nil)
(10)

CL-USER 5 > (cons 10 'nil)
(10)

One than has to use the result list: store it somewhere, pass it to a function, ...

push doesn't work with literals as second argument since it is supposed to mutate the place indicated by second argument. The empty list is no place and it cannot be redefined.

push is a macro that does different things for different looking second argument. You can check what it does by looking at the maroexpand-1:

(macroexpand-1 '(push 5 binding))
; ==> (set1 binding (cons 5 binding))

(macroexpand-1 '(push 5 (car structure)))
; ==> (let* ((tmp structure))
;       (rplaca tmp (cons 5 (car tmp))))

How it knows what to do is by looking at the setf function. eg. calling:

(get-setf-expansion 'binding)
; ==> () 
;     ()
;     (tmp)
;     (setq binding tmp)
;     binding

You can define your own for eg. a class such that you can push elements to an object of a class you have created and make it do something special. Eg. you are extending the language feature of push such that it also supports a class you have made.

So what happens when I evaluate (get-setf-expansion ''()) well you get the reason why the expansion of (macroexpand-1 '(push 3 '()) turns into:

(let* ((tmp (cons 3 '())))
  (funcall #'(setf quote) tmp '())) 
 

And there is the source of your unusual error message.

Related