True destructive pop?

True destructive pop?

In #elisp, the pop macro is not really destructive in the sense that you can use it to manipulate a memory location regardless of scope. It's only as destructive as setf, i.e. it operates on "generalized places".

By contrast, destructive functions like delete make use of the lower-level setcar and setcdr internally.

That tells us basically how to make a "true" pop:

(defun true-pop (list)
  (setcar list (cadr list))
  (setcdr list (cddr list)))

But something happens when the list has only one element left…

(setq foo '(a))
(true-pop foo)
foo
=> (nil)

Compare with pop:

(setq foo '(a))
(pop foo)
foo
=> nil

There seems to be a fundamental limitation in Emacs Lisp: you can't take a memory address that points to a cons cell and make that cons cell be not a cons cell. Even delq ceases to have a side-effect when the list has one element left: (delq 'a foo) does not change foo.

Why it works with pop? Because you use it on a symbol that's in-scope, and it reassigns the symbol to a different memory address. Or that's how I understood it as of [2024-09-12 Thu].

Workaround

There is a trick, if you still need "true pop" behavior. Let's say there's a list that you need to consume destructively, but the list is not stored in a symbol in the global obarray, but in a hash table value. To look up the hash table location every time would cost compute. So here's what we do: access the hash table once, let-bind the value, and manipulate only the cdr cell of the value.

How the value might originally be stored:

(puthash KEY (cons t (make-list 100 "item")) TABLE)

Note that the car is just t, and we'll do nothing with it.

Now, consuming the list:

(let ((items (gethash KEY TABLE)))
  (while (cdr items)
    ... (DO-SOMETHING-WITH (cadr items)) ...
    (setcdr items (cddr items))))
Created (3 months ago)