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!

Drew
  • 75,699
  • 9
  • 109
  • 225
R1ck77
  • 113
  • 3

1 Answers1

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))))
Oliver Scholz
  • 846
  • 7
  • 12
  • 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. –  Nov 01 '18 at 13:07
  • 1
    Certainly! It's more a "rule of thumb" thing. The case you mention is basically a common idiom. – Oliver Scholz Nov 01 '18 at 13:12
  • thank you very much for the answer @OliverScholz! Oh my god, I knew I was a newbie, but I didn't realize I was so clueless... I admit that this " '(x y z) is a singleton" thing is counter-intuitive: is this an elisp idiosyncrasy, or something common to all lisp? – R1ck77 Nov 01 '18 at 16:02
  • Also, both OliverScholz and @DoMiNeLa10, thank you for the suggestion about the code. I use associative lists for the cache, and I wholeheartely agree about using values and not mutables (I come from Clojure btw where it's a core principle :) ), but due to my ignorance the most contorted route between two points was the shortest :) (and this covers the use of '(nil . nil) instead of just nil. I wasn't able to make it work with nil. Don't ask :D) – R1ck77 Nov 01 '18 at 16:08
  • The full code is here, should it be a rainy day and you need a good laugh: https://github.com/R1ck77/elisp-rest-mode-test/blob/master/src/rest-state.el – R1ck77 Nov 01 '18 at 16:10
  • 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)`. – Oliver Scholz Nov 01 '18 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. – Oliver Scholz Nov 01 '18 at 17:43
  • The idea of using the lexical binding on the file could be worth investigating (as a bonus, I will be more at home). I admit I am still recovering from the quote thing, and trying to digest it. I knew that emacs lisp is an archaic language when I started learning it - no disrespect intended - but I didn't expect to have such a hard time getting my head around it :) Anyway, this answer (and the one to the question I have duplicated) taught me a lot, thank you! – R1ck77 Nov 01 '18 at 18:21
  • 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."](http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/speope_quote.html), Scheme spec: ["it is an error to modify constant objects"](https://groups.csail.mit.edu/mac/ftpdir/scheme-reports/r5rs-html/r5rs_8.html). Some implementations give a nicer error when you do that (actually Emacs does as well for preloaded purified constants). – npostavs Nov 01 '18 at 23:12