reddit / emacs
Any way to use something like setf on a let-bound pointer to another variable, and affect that variable?
meedstrom · 2022-10-30 21:11
Suppose I have a table and I work with one row at a time, is there any elegant way to set one cell to something different? If I try this:
(defvar my-table (make-hash-table))
(puthash 'foo '(nil nil 0) my-table)
(puthash 'bar '(nil nil 0) my-table)
(puthash 'baz '(nil nil 0) my-table)
(defun do-stuff ()
(let ((running (nth 0 (gethash key my-table)))
(chain-state (nth 1 (gethash key my-table)))
(last-idle-value (nth 2 (gethash key my-table))))
(setf running t)))
(do-stuff 'bar)
then I would hope that `(gethash 'bar my-table)` shows me the row `(t nil 0)` but it's still `(nil nil 0)`.
This matters because now the only thing that will work is if I don't let-bind. And that makes my large functions super-unreadable with `(nth 1 (gethash key my-table))` all over the place.
I've also tried `cl-letf` and `seq-let`, they don't help here.
pedzsanReddit · 2022-10-30 22:08
I'm not sure if you noticed but `do-stuff` has no argument. I assume it is suppose to be `key`.
As far as your general question, I don't know. I don't fully understand `self`.
JDRiverRun · 2022-10-30 22:34
setf, pop, push, and others work on *generalized places*. To see all the "places" they can target, check out the info help on "generalized variable" (both elisp and cl, which adds a bunch more). setf and friends are macros, so they look at the form of the *place* and do what's necessary to make the changes. Like this:
(setf (car (gethash key my-table)) t)
But setting the first element is simple, so this is also a good approach:
(let ((entry (gethash 'bar my-table)))
(setcar entry t))
Quite different, yes? Now go to the end of the first setf form and `M-x pp-macroexpand-last-sexp`. You'll see the macro expansion:
(let*
((v
(gethash key my-table)))
(setcar v t))
Looking familiar? So now you see it — setf is *just a convenience macro* which expands to elisp you probably already know. Well worth doing some expansions to look behind the curtain.
BTW, if you find your data structures start to get too ugly to understand, after exhausting simple lists, hashes, vectors, alists, and plists, you can look into [cl-defstruct](https://www.gnu.org/software/emacs/manual/html_node/cl/Structures.html) to get named slots.
meedstrom · 2022-10-31 10:10
True. And too late to edit, oh well.
meedstrom · 2022-10-31 10:48
I hadn't realized `setf` and `pop` were macros, but of course they are. Good idea on macroexpanding them.
Looks like my `do-stuff` example could be written like this. Although of course it's not totally elegant yet having to write `(nth 0 row)`, `(nth 1 row)`, `(nth 2 row)`.
(defun do-stuff (key)
(let ((row (gethash key my-table)))
(setf (nth 0 row) t)))
Thanks for pointing me to info. Could define new generalized targets with `gv-define-simple-setter`, although not on let-bound variables, but you could perhaps make a tiny function that makes *any* let-bound variable accessible. Otherwise, looks like `gv-letplace` could do what I originally wanted.
I went with a different approach. A sort of poor-man's `cl-defstruct`, with a global accessor `(my-value)`. It's not a fully compressed notation, having me insert `key` everywhere, but it won't invite bugs unlike the `nth 0` business and it's not more or less verbose than `cl-defstruct`. My original hypothetical `do-stuff` would have been wonderful, but if possible, looks to require some boilerplate to set up I don't understand.
(defvar my-table (make-hash-table))
(puthash 'foo '((running . nil)
(chain-state . nil)
(last-idle-value . 0))
my-table)
(puthash 'bar '((running . nil)
(chain-state . nil)
(last-idle-value . 0))
my-table)
(defmacro my-value (key subkey)
(require 'map)
`(map-elt (map-elt my-table ,key) ,subkey))
(defun do-stuff (key)
(setf (my-value key 'running) t)))
(do-stuff 'bar)
arthurno1 · 2022-10-31 11:57
What is hindering you to use a [class](https://www.gnu.org/software/emacs/manual/html_mono/eieio.html) or [struct](https://www.gnu.org/software/emacs/manual/html_node/cl/Structures.html), so you can use named elements instead of anonymous list elements, since you already are using a list to represent a state as an object?
github-alphapapa · 2022-10-31 16:40
In addition to the other comments, what's missing is a mention of `cl-symbol-macrolet`, which would do what you're asking for. i.e.
(defun do-stuff ()
(cl-symbol-macrolet ((running (nth 0 (gethash key my-table))))
(let ((chain-state (nth 1 (gethash key my-table)))
(last-idle-value (nth 2 (gethash key my-table))))
(setf running t))))
Besides that, as was mentioned, `cl-defstruct` is your friend. It makes for much more readable code (and often more efficient, too).
meedstrom · 2022-11-01 00:13
Thank you!! What a gem!
One day I hope to make it behave like `seq-let` so we have my dream let, "cl-symbol-seq-macrolet" :-)
JDRiverRun · 2022-11-01 16:08
Nice addition. In fact, this reminds me of another useful trick I came up with when I tired of writing out the accessors: you can combine the two (cl-defstruct and cl-symbol-macrolet) to make access even easier (see [this gist](https://gist.github.com/jdtsmith/5bc37c269dec2436725fd2e1fa788a54)).
SlowValue · 2022-11-01 17:49
But beware, the macro is called every time you refer to that variable, so it is no pointer:
(let ((foo 1))
(cl-symbol-macrolet ((bar (cl-incf foo)))
bar
bar
bar)
foo)
returns `4`
github-alphapapa · 2022-11-04 18:20
That's interesting, thanks. I wonder how the performance of the expanded code in that macro compares to using the defined accessor functions, which have optimized compiler macros.
github-alphapapa · 2022-11-04 18:22
> One day I hope to make it behave like seq-let so we have my dream let, "cl-symbol-seq-macrolet" :-)
That's an interesting idea. What I would love to see, myself, is a version of `pcase-let` that acts like `symbol-macrolet`, so one could destructure various things for access, but also set the destructured places using the bound symbols.
JDRiverRun · 2022-11-04 22:43
Thanks for this comment. I looked at the expansions and of course deep down they're all just checking the structure type and finally doing `(aset vec 1)`. But my original "simple" version with `cl-struct-slot-value` was in addition, on each and every write:
1. checking if it's dealing with a list or record as the underlying data type in a *very* complex way, and
2. looking up the vector index for each slot via a hash table which maps slot-name to slot index.
What looked like a "simpler" macro actually expanded to several pages of code with a number of complex function calls!
So I rewrote it to simply use the slot accessors *directly*. The macro looks a bit uglier now, but produces *identical* code as the long form method would. Lesson learned: always expand your macros. I think I'll be using this pattern quite a bit. Instead of:
(setf (my-struct-foo my-struct) (+ 123 (my-struct-bar my-struct))
(my-struct-bar my-struct) (+ 456 (my-struct-bar my-struct)))
use:
(with-convenient-slots (foo bar) my-struct
(setq foo (+ 123 bar)
bar (+ 456 foo)))
And if you are always accessing the same global struct, you can hard-code that into the macro and use it like:
(with-my-struct-slots (foo bar)
(setq foo (+ 123 bar)
bar (+ 456 foo)))
`cl-symbol-macrolet` and `cl-defstruct` make a powerful combo.
meedstrom · 2022-11-15 11:49
Thanks for updating the gist! Seems you've extended `cl-symbol-macrolet` to take bindings like `map-let`. I love it.
Must say that with this in hand, I find less point in having structs; the data type could often be an alist or hash table. That could simplify your code, in terms of using fewer computer science concepts.
Found something mildly interesting: if I replace your use of `intern` with `make-symbol`, it doesn't work. Do you know why?
meedstrom · 2022-11-15 11:55
Like `map-let`? You've seen JDRiverRun's updated gist by now, of course, which does that :)
Or would you like something called, say, `with-all-slots` that simply binds all the keys in the object/alist/table, so you don't even have to specify which ones you'll use? Seems a bit prone to unexpected bugs though, in case one of the keys reuses a name in the global namespace -- safer to make it explicit.
JDRiverRun · 2022-11-15 14:50
>Found something mildly interesting: if I replace your use of intern with make-symbol, it doesn't work. Do you know why?
Because make-symbol does (emphasis added):
>Return a newly allocated **uninterned** symbol whose name is NAME.
"Interned" is just a fancy way of saying "on a global list of symbols each of which has a variable and function cell". You are after the interned symbol's function cell. Try evaluating these to clarify:
(symbol-function 'ignore)
(symbol-function (intern "ignore"))
(symbol-function (make-symbol "ignore"))
BTW, you can actually inspect that "global list of symbols"; it's called `obarray` (check `(seq-length obarray)`).
> I find less point in having structs; the data type could often be an alist or hash table.
You can of course omit the defstruct entirely in favor of rolling your own underlying data structure, but you'll find that struct access expands to some very tight vector/record cell access, which is likely faster than list access for larger structs, and faster than hashes for most. Plus it does runtime type checking for free. And gets upgraded to even faster structures over time.
Do note that the most recent version is faster but at the cost of being a bit more fragile, as it hard-codes the name of the defstruct slot accessors, which can be altered with `:conc-name`.
BTW, I discovered the source of this trick: the [with-slots macro of eieio](https://github.com/emacs-mirror/emacs/blob/c63d77ac6b10dc453d08afc852debc6a9a3cde36/lisp/emacs-lisp/eieio.el#L333).
github-alphapapa · 2022-11-18 21:38
> Like map-let? You've seen JDRiverRun's updated gist by now, of course, which does that :)
`map-let` doesn't bind symbols to places that can be set with `setf`.
> Or would you like something called, say, with-all-slots that simply binds all the keys in the object/alist/table, so you don't even have to specify which ones you'll use? Seems a bit prone to unexpected bugs though, in case one of the keys reuses a name in the global namespace -- safer to make it explicit.
No, that would not be a good idea. And you can see some other implementations of such ideas that I cataloged here: https://github.com/alphapapa/emacs-package-dev-handbook