reddit / emacs

How to avoid persistent alist values in a function?

rberaldo · 2022-10-19 01:44
I've been using an alist as a means of counting occurrences of characters in a string. However, the values associated with a key in the alist are persistent across function calls, even though I reset the alist in a `let` form (and, well, it's inside a function!). Here is a minimal working example. Calling `my/test` multiple times continually increases the alist values: (defun my/test () (let ((my/alist '((a . 0) (b . 0)))) (incf (alist-get 'a my/alist)) (incf (alist-get 'b my/alist)) my/alist)) What am I doing wrong? Something tells me the problem is with `incf`, but I cannot understand how `my/alist` is stored in memory, since it should have its scope inside the function and be reset when `let` is run.
ieure · 2022-10-19 02:06
You made accidentally self-modifying code. I suggest you call `(symbol-function 'my/test)` after defining it, and again in between evaluations of it. This should show clearly what's going on. The short answer is that in Lisp, code and data are the same thing. So when you include a quoted list like that, the literal value is included in your function definition, and each call mutates the value.
agrostis · 2022-10-19 02:27
Essentially, your alist literal is stored as part of a global value in the heap, and each new invocation of ``my/test`` operates on a “new” reference to it. One possible way around it is to cons a new alist on each invocation, e. g. like this: (defun my/test () (let ((my/alist `((a . ,0) (b . ,0)))) (incf (alist-get 'a my/alist)) (incf (alist-get 'b my/alist)) my/alist))
gnomebodieshome · 2022-10-19 02:50
Your let is referencing the same literal list that is embedded in the code wherever that ends up with the rest of the literal code - whatever form that takes depending on the implementation. The single quote protects it from being evaluated on being read in, but then that same object is bound to my/alist every time. You need to actually create it on each function invocation.
itistheblurstoftimes · 2022-10-19 03:14
Create the alist with (list (cons 'a 0) (cons 'b 0)) and it'll work like you expect. Almost identical to a question I asked 3 years ago. https://www.reddit.com/r/emacs/comments/bscsqe/elisp_alist_question/
[deleted] · 2022-10-19 03:15
[deleted]
7890yuiop · 2022-10-19 04:10
It's very important to understand that `'...` which means `(quote ...)` is not a shorthand for making lists. It's a form which, when evaluated, returns the object that was created by the lisp reader (at read time) -- in whatever state that object is currently in (at eval time). So if you quote a list and then *modify* that list, it remains modified. If you don't want such modifications to be permanent, you should create the list with `(list ...)` which creates a new list object at eval time. The following example illustrates what is going on. I strongly recommend doing some reading about the distinct `read` and `eval` phases of lisp execution. Once you understand that distinction, `quote` will make much more sense. (defun foo () (list 'a 'b 'c)) => foo (defun bar () '(a b c)) => bar (foo) => (a b c) (bar) => (a b c) (delq 'b (foo)) => (a c) (delq 'b (bar)) => (a c) (foo) => (a b c) (bar) => (a c) (symbol-function 'foo) => (closure (t) nil (list 'a 'b 'c)) (symbol-function 'bar) => (closure (t) nil '(a c))
AntlerBaskets · 2022-10-19 06:21
Is this not the case under lexical scope (ie. OP's code would be valid)? I'm used to thinking about lexical closures because of experience with other Lisps, which I think would have counterproductively increased my confusion had I ran into this myself;I probably haven't because it's an artifact of Elisp's traditional dynamic scope, right?
[deleted] · 2022-10-19 06:54
I always found the backquote difficult to understand, so here's a more verbose version of what's happening: (defun my-test-quoted () (let ((my-alist (quote ((a . 0) (b . 0))))) (cl-incf (alist-get 'a my-alist)) (cl-incf (alist-get 'b my-alist)) my-alist)) (my-test-quoted) => ((a . 1) (b . 1)) ((a . 2) (b . 2)) ((a . 3) (b . 3)) (defun my-test-lexical () (let ((my-alist (list (cons 'a 0) (cons 'b 0)))) (cl-incf (alist-get 'a my-alist)) (cl-incf (alist-get 'b my-alist)) my-alist)) (my-test-lexical) => ((a . 1) (b . 1)) ((a . 1) (b . 1)) ((a . 1) (b . 1))
SlowValue · 2022-10-19 07:50
> because it's an artifact of Elisp's traditional dynamic scope, right? no its not. Here is Common Lisp code showing the same effect in sbcl v2.2.9: (defun my/test () (let ((my/alist '((a . 0) (b . 0)))) (incf (cdr (assoc 'a my/alist))) (incf (cdr (assoc 'b my/alist))) my/alist)) ;; cl-user> (my/test) ((a . 1) (b . 1)) cl-user> (my/test) ((a . 2) (b . 2)) cl-user> (my/test) ((a . 3) (b . 3)) --- > lexical closures lexical closures are also called "let over lambda". OP's code shows a let in a lambda (one could translate that as "let under lambda", but of course that term isn't used anywhere) and this is no lexical closure.
SlowValue · 2022-10-19 08:00
For me this is the best answer, because you explained it with the lisp reader, which should make it clear.
gnomebodieshome · 2022-10-19 15:06
It is a closure over the same specific object every time it runs, for my lack of better words. Single quote is the same as (quote object) which just puts that object in that place among the rest of the objects that is the code. It is funky coming from other languages, but actually makes sense in terms of its homoiconicity with what you can do with it.