1

Glad that the title got your attention :D

I have this little snippet that's driving me crazy:

(defvar cache '(nil . nil))

(defun init-cache ()
  (setq cache '(nil . nil)))

(defun save (value) 
  (nconc cache (list value)))

It's the minimum reproducible example out of a cache I'm trying to implement. I would expect the save to add elements to the list, and the init-cache to reset the cache.

Instead:

(init-cache)  ; cache is (nil)
(save "foo")  ; cache is (nil "foo"). So far, so good
(init-cache)  ; cache should be (nil), and it's all downward from here…
(save "bar")  ; (nil "foo" "bar") damn you, "foo"! Why are you here? Why are you doing this to me?!?!

So from my un-educated point of view, it looks like setq isn't working! I'm just changing the cdr of cache with nconc, why can't I change the cache symbol's value???

So my questions are:

  • what's happening?
  • the code looks awkward as hell: is there a more idiomatic way (one that works, if possible =) )

Thank you!

0

1 Answer 1

5

When writing list or cons constants in source code there is actually a subtle difference between e. g. (cons 'first 'second) and '(first . second). The latter denotes a constant cons cell in your code, the former creates a new cons cell each time the function is called.

What happens in init-cache is that the global variable cache is set to point to the same cons cell again and again that it was already referencing after the first call. What you want instead is that it creates a new cons cell on each call:

(defun init-cache ()
  (setq cache (cons nil nil)))

For the sake of completeness I might also mention that (defun init-cache () (setcdr cache nil)) would also "reset the cache". The variable cache would still point to the same cons cell, but init-cache would set its cdr to nil.

However, it is generally good practice to avoid messing with 'destructive' functions altogether, unless there is a very strong and compelling reason for it, like handling large data in a performance critical loop. As a default, I would always choose non-desctructive handling of lists as this here is exactly the kind of thing that might bite you. I would even ditch nconc in favour of (setq <variable> (append ...)) just on general principles. (See DoMiNeLa10's comment below for an typical exception.)

A more idiomatic way

I'm assuming that there is no particular reason to initialize cache as '(nil . nil), since you didn't mention one. So I'd write the above like this:

(defvar cache nil)

(defun init-cache ()
  (setq cache nil))

;; Alternative 1: If there is no particular reason to have newer elements
;; at the end of the list, the most idiomatic way is to prepend them. (And
;; possibly reverse the list locally, if needed.)

(defun save (value)
  ;; Or: (push value cache)
  (setq cache
        (cons value cache)))


;; Alternative 2: If there _is_ a reason to have newer elements at the end,
;; I'd use `append`.

    (defun save (value)
      (setq cache
            (append cache
                    (list value))))
9
  • 2
    What about accumulating data to a list with push and then using nreverse on it when returning it to reverse the order back to being correct? Assuming the list itself is generated in that function and nothing touches said list before the function is done with it, I see no problem with using nreverse. I think there are some cases where destructive functions can be used safely without much reasoning.
    – user12563
    Nov 1, 2018 at 13:07
  • 1
    Certainly! It's more a "rule of thumb" thing. The case you mention is basically a common idiom. Nov 1, 2018 at 13:12
  • 2
    I see, mutating the cache is the side effect of a getter function. Your trouble with nconc probably stems from the fact that nconc doesn't alter the car of a cons cell; that's why you normally see (setq list-var (nconc list-var other-list). Nov 1, 2018 at 17:39
  • 1
    You could pass around hash tables or set lexical-binding file-locally to t and do something with closures, like in Scheme. As to whether that handling of constants is a particulairy of Elisp: I honestly don't remember. Elisp is the only Lisp I've been touching for a long while. But I would think that this is common to most major dialects: It's understandable to be surprised by this, but when you think about it, it's logical: (cons 'x 'y) is a function call, but (quote (x . y)) is a special form. Nov 1, 2018 at 17:43
  • 1
    @R1ck77 "is this an elisp idiosyncrasy, or something common to all list?" - most Lisps: Common Lisp spec: "The consequences are undefined if literal objects (including quoted objects) are destructively modified.", Scheme spec: "it is an error to modify constant objects". Some implementations give a nicer error when you do that (actually Emacs does as well for preloaded purified constants).
    – npostavs
    Nov 1, 2018 at 23:12

Not the answer you're looking for? Browse other questions tagged or ask your own question.