Pop first element in a list using a macro

Viewed 79

I have the following code

(defmacro popm(l)
`(prog1 (car ,l)
    (setf ,l (cdr ,l))))

which is supposed to pop the first element out of the list. However, I don't know how to call the macro. I tried using (popm (2 3 4)) but I get the error "EVAL: 2 is not a function name; try using a symbol instead". Can anyone point me in the right direction?

2 Answers

The correct way to debug macros is to use macroexpand-1:

(macroexpand-1 '(popm (1 2 3)))
(PROG1 (CAR (1 2 3)) (SETF (1 2 3) (CDR (1 2 3))))

Now it should be clear why your macro does not work: it cannot be used with constant objects.

It does work with variables though:

(defparameter a '(1 2 3))
(popm a)
==> 1
a
==> (2 3)

First of all, don't modify literals.

Secondly, as a macro which pops a value from a list held in a variable (or, more generally a place), your macro is unsafe, because it evaluates its argument more than once. Instead it should be this:

(defmacro popm (place)
  (let ((<v> (make-symbol "V")))
    `(let ((,<v> ,place))               ;evaluate PLACE only once
       (prog1 (car ,<v>)
         (setf ,place (cdr ,<v>))))))

Finally if what you want to do is mutate list structure (but not literals!) you can do that, and you don't need a macro. Here is a function which almost does that:

(defun pop-list (l)
  ;; BROKEN
  (prog1 (car l)
    (setf (car l) (cadr l)
          (cdr l) (cddr l))))

This seems to work:

> (let ((l (list 1 2 3))) ;note not a literal!
    (values (pop-list l)
            l))
1
(2 3)

So that looks good. Except it doesn't actually work:

> (let ((l (list 1 2 3)))
    (values (pop-list l)
            (pop-list l)
            (pop-list l)
            l))
1
2
3
(nil)

It can't work because at the point where the list is a single cons it can't deal with that (this is why popm needs to be a macro). There are two solutions to that: one is to say that, well, after it's popped all the contents, it just returns nil all the time. But then you can't know if the list is empty or not. So, well, here's another approach:

(defun pop-list (l)
  ;; Returns two values: the thing popped, and whether this was the
  ;; last thing.  Popping a broken heart is an error.
  (let ((broken-heart (load-time-value (make-symbol "BROKEN-HEART") t)))
    (cond
     ((eq (car l) broken-heart)
      (error "broken heart"))
     ((null (cdr l))
      (values (prog1 (car l)
                (setf (car l) broken-heart))
              t))
     (t
      (values (prog1 (car l)
                (setf (car l) (cadr l)
                      (cdr l) (cddr l)))
              nil)))))

And now

> (let ((l (list 1 2 3)))
    (pop-list l))
1
nil

> (let ((l (list 1 2 3)))
    (pop-list l)
    (pop-list l)
    (pop-list l))
3
t

> (let ((l (list 1 2 3)))
    (pop-list l)
    (pop-list l)
    (pop-list l)
    (pop-list l))

Error: broken heart
Related