reddit / emacs
Let-bindings ignored for constant lists and sequences?
cottasteel · 2022-11-22 09:50
I ran into a strange problem involving the let-bindings of variables to constant lists and sequences.
Suppose we have the function `foo1` which seeks to record unique buffer positions in a list, starting at the point, moving the cursor up until we have either recorded three unique positions or reached the beginning of the buffer:
(defun foo1 ()
(save-excursion
(let ((baz '(blank blank blank)))
(while (and (member 'blank baz) (not (bobp)))
(cl-loop for item in-ref baz
with pt = (point)
when (eq item 'blank)
do (unless (member pt baz)
(setf item pt)))
(when (member 'blank baz)
(previous-line)))
baz)))
I understand this is not the best way to achieve this functionality, but it illustrates the problem. We apply `foo1` to a buffer containing the GPL3 info:
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.<*>
...
Here `<*>` indicates the point. If we invoke foo1 at the point we obtain the correct result `(947 927 857)`. However, if we then, for example, move the cursor to the left and invoke `foo1` again, we obtain the same result `(947 927 857)`, even though the point has changed.
We can fix this by replacing the constant list `'(blank blank blank)` in the let-binding with the function invocation `(make-list 3 'blank)` to obtain the function `foo2`:
(defun foo2 ()
(save-excursion
(let ((baz (make-list 3 'blank)))
(while (and (member 'blank baz) (not (bobp)))
(cl-loop for item in-ref baz
with pt = (point)
when (eq item 'blank)
do (unless (member pt baz)
(setf item pt)))
(when (member 'blank baz)
(previous-line)))
baz)))
One can verify that invoking `foo2` in the same way as `foo1` above to the GPL3 buffer, we obtain the correct results `(947 927 857)` and `(946 876 804)`.
Why do repeated invocations of a function cause the let-bindings of constant lists and sequences to be ignored? Is this some kind of caching optimization?
I have these function implemented in a file with lexical scoping enabled. Is this an issue of lexical vs dynamic scoping?
twleo · 2022-11-22 10:21
`eq` vs `equal` things?
7890yuiop · 2022-11-22 12:03
You've written self-modifying code, which is modifying your intended "constant" list.
Use `(symbol-function 'foo1)` to observe what evaluating that function has
done to your code.
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 use `(list ...)`
or similar (`make-list`, in your `foo2` variant) to create a new list object
at eval time.
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.
cottasteel · 2022-11-22 14:54
Thanks for the explanation. You're right, I was assuming that `(quote ...)` was a mechanism for creating constant lists. I vaguely remember reading a guideline for elisp that one should always initialize aggregate data types like lists and vectors with a function, but I didn't understand why this was necessary.
Is there a scenario where using a `(quote ...)` to define an object in a function is a desirable thing to do?
7890yuiop · 2022-11-23 00:40
> Is there a scenario where using a `(quote ...)` to define an object in a function is a desirable thing to do?
Firstly, note that we do this *all the time* for symbols especially. When we write `'foo` we are simply obtaining (at eval-time) the symbol that the lisp reader saw (and interned) at read-time.
For the likes of lists, if the data *is* certain to be constant in practice, then using `quote` is absolutely fine (and more efficient for not re-generating the list each eval). That wasn't the case in your example, but there's plenty of cases where this is true. For example, *unless* the code was using the return value as a list (in any potentially-destructive manner), then I would certainly use `(memq VAR '(foo bar baz))` in preference to `(memq VAR (list 'foo 'bar 'baz))` if the code merely required a boolean result for the membership test.
cottasteel · 2022-11-23 08:15
I understand how it works for immutable objects like symbols and mutable objects that we are certain won't be modified. My question was about a scenario like `foo1` with self-modifying code. Does that have any reasonable use case? It seems like it would be useful if one wants persistent function-local variables, for example, to count how many times a particular function was invoked.
7890yuiop · 2022-11-23 09:13
Intentional self-modifying code is almost undoubtedly for the sake of efficiency. I suspect you'd struggle to find anyone *recommending* that as an approach, though, and I don't have any particular example for you.
artlogic · 2022-11-23 10:19
After reading this I was still a bit confused, so I took a look at the official docs for `quote`, which cleared things up for me:
> This special form returns object, without evaluating it. The returned value might be shared and should not be modified.
https://www.gnu.org/software/emacs/manual/html_node/elisp/Quoting.html
deaddyfreddy · 2022-11-23 12:23
Sorry don't get it, why there's `cl-loop`, `setf` etc if the only thing you need is to collect 3 points one above another?
cottasteel · 2022-11-23 12:35
As I said in the post, this is just an example used to illustrate the problem with modifying a list initialized using `'(...)`. Of course there are much simpler ways to collect 3 points one above another, but that is not my ultimate goal. The application I'm working on is more complicated and has a lot of details that are irrelevant to the question I asked, but it has a similar structure to the functions `foo1` and `foo2`.
u/7890yuiop explained in [this comment](https://www.reddit.com/r/emacs/comments/z1plya/comment/ixcfz6a/?utm_source=share&utm_medium=web2x&context=3) that, in general, it is not a good idea to modify objects defined with `(quote ...)`
7890yuiop · 2022-11-23 12:36
I'll just repeat my best advice regarding all of this:
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.
(As will various other things.)
deaddyfreddy · 2022-11-23 14:43
> in general, it is not a good idea to modify objects
since mutability is the main source of bugs
artlogic · 2022-11-23 15:13
I suspect what you're saying is that `quote` returns the specific object generated at `read` time - is that right?
That seems like a good rule of thumb to follow, but the elisp manual I quoted and linked seems to tell a more subtle story, saying the object returned *may* be shared. To me, that implies an internal optimization that's occurring. I didn't see anything explicitly stating that `quote` is essentially returning a reference to the AST.
I wonder if this all comes down to implementation. Do you have experience with other lisps? Do most others act in this way as well?
7890yuiop · 2022-11-23 21:31
Yes, I believe the "shared" warning is a comment on potential byte-compiler optimisations, rather than `quote` as a general mechanism.
See `C-h f quote` for a more direct comment about the lisp reader, but note that this is also implicit in the text which referred to the "unevaluated" argument being returned, as that leaves only the lisp reader as the source for the returned object.
To my knowledge this is very much the standard definition of `quote` across lisps in general.
JDRiverRun · 2022-11-24 01:31
(defun make-list () '(a b c))
(eq (make-list) (make-list)) ; -> t
For those who know Python, this is similar to the commonly encountered issue:
In [113]: def lis(l=(1,2,3)): return l
In [114]: lis() is lis()
Out[114]: True