wpcarro
wpcarro

Reputation: 1546

Update struct field

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

Answers (2)

wpcarro
wpcarro

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

Steve Vinoski
Steve Vinoski

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

Related Questions