Reputation: 1546
Is there a more ergonomic way to apply a function to a field in a struct in Elisp?
Say I have the following:
(cl-defstruct stack xs)
(defvar stack (make-stack :xs '(1 2 3 4 5)))
Is there an easy way to apply functions to the :xs
field. I'd like an API like this:
(update-field :xs stack (lambda (xs)
(cl-map 'list #'1+ '(1 2 3 4 5))))
Does anyone know if this exists?
Update:
I'm looking for a way to DRY up the calls to (stack-xs stack)
(see below). What I'm looking for is more similar to something like Map.update
from Elixir.
(setf (stack-xs stack) (cl-map 'list #'1+ (stack-xs stack)))
Upvotes: 4
Views: 818
Reputation: 1546
I ended up solving this by writing a macro, struct/update
:
(defmacro struct/update (type field f xs)
"Apply F to FIELD in XS, which is a struct of TYPE.
This is immutable."
(let ((copier (->> type
symbol-name
(string/prepend "copy-")
intern))
(accessor (->> field
symbol-name
(string/prepend (string/concat (symbol-name type) "-"))
intern)))
`(let ((copy (,copier ,xs)))
(setf (,accessor copy) (funcall ,f (,accessor copy)))
copy)))
I use this macro as such:
(defun set/add (x xs)
"Add X to set XS."
(struct/update set
xs
(lambda (table)
(let ((table-copy (ht-copy table)))
(ht-set table-copy x 10)
table-copy))
xs))
Which will update the following struct:
(cl-defstruct set xs)
Upvotes: 0
Reputation: 20024
The cl-defstruct
macro creates slot accessors of the form NAME-SLOT
where NAME
is the struct type name, and SLOT
is the slot name. Using your example, you can set the xs
slot using setf
with the slot accessor like this:
(cl-defstruct stack xs)
(defvar st (make-stack :xs '(1 2 3 4 5)))
(setf (stack-xs st) (cl-map 'list #'1+ (stack-xs st)))
(stack-xs st)
The final line above returns '(2 3 4 5 6)
.
Update: a downside of the setf
call shown above is that the slot accessor has to be used twice, once to read the current value and then again to update it to the new value. You can use cl-callf
to DRY it up:
(cl-callf (lambda (p) (cl-map 'list #'1+ p)) (stack-xs st))
Alternatively you could wrap the setf
call within a new method defined on the stack
type using cl-defmethod
, perhaps like this:
(cl-defmethod stack-update ((s stack) f slot)
"Update SLOT in stack S by applying F.
F is passed one argument, the current value of SLOT,
and is expected to return a new value for SLOT."
(let ((sl (if (keywordp slot) (intern (substring (symbol-name slot) 1)) slot)))
(setf (cl-struct-slot-value 'stack sl s) (funcall f (cl-struct-slot-value 'stack sl s)))))
You can then call stack-update
like this:
(stack-update st #'(lambda (p) (cl-map 'list #'1+ p)) :xs)
or equivalently:
(stack-update st (apply-partially 'cl-map 'list #'1+) 'xs)
Upvotes: 2