Subject: Re: bug#77058: [PATCH] Add cl-with-accessors

From: [email-masked]
Date: Sat, 29 Mar 2025 23:56:48 +0000

Hello,

I apologize for missing Stefan's response.

Daniel Colascione wrote:
> Eli Zaretskii <[email-masked]> writes:
> 
>> Ping! Okamsn, would you like to submit an updated patch?
>>
 >>> Stefan Monnier wrote:>>>> Also, in the test 
`cl-lib-struct-accessors' in
>>>> test/lisp/emacs-lisp/cl-lib-tests.el, it checks for the keyword
>>>> `:readonly', but the documented keyword is `:read-only'.  Is this a
>>>> typo, or was this intentional?
>>>
>>> I'd go with "typo".

I've added a separate patch file to change it from `:readonly' to 
`:read-only'.

>>> It'd be worthwhile to mention that it also works with accessors of other
>>> objects than structs, e.g. `car`, or `process-filter`.
 >>> ...
>>> so maybe it's worth mentioning `with-slots` (which arguably should be
>>> moved to `cl-lib`).

I've mentioned `with-slots' and that `cl-with-accessors' works with `car'.

>>> I think this should be moved to a CL-Lib subsection in the
>>> "package-specific changes" section.

Done. I did not move the entry about `cl-type-of'.  I see that it is 
written in C.

>>> I'd use `VAR` or `SYMBOL` rather than `SYMBOL-NAME`.
>>> Probably similarly use `ACCESSOR` rather than `ACCESSOR-NAME`.
>>> ...
>>> Should be lower case `&rest` on this last line.
 >>> ...
>>> I think `place` is wrong here.  `(person-age FOO)` is a "place" but
>>> `person-age` is not (or, well, it is but not the right one: when taken
>>> as a place it refers to the variable, not the accessor).
>>> I think it should just be `symbolp`.
>>> ...
>>> Please use the same format as other similar warnings we have, such as:
>>>

I've made these changes. I changed SYMBOL-NAME to NAME to be consistent 
with `cl-symbol-macrolet'.

> ...
 > You could get away from> the need for compile-time visible of classes 
by switching from
> cl-symbol-macrolet to a custom expander function (which is how
> cl-symbol-macrolet itself is implemented), but I was too lazy.

Daniel,

I wrote `cl-with-accessors' with `cl-symbol-macrolet' because the 
definition at http://clhs.lisp.se/Body/m_w_acce.htm used 
`symbol-macrolet'. Do you believe that it should be done differently or 
be more like what you wrote?

Thank you.
From 2877ea4cc54402535edb6f5d8d50a2a7ae1c79cc Mon Sep 17 00:00:00 2001
From: Earl Hyatt <[email-masked]>
Date: Sat, 29 Mar 2025 17:30:48 -0400
Subject: [PATCH v3 1/2] Fix typo in test of read-only cl-defstruct slot.

* test/lisp/emacs-lisp/cl-lib-tests.el (mystruct)
(cl-lib-struct-accessors): Use ":read-only" instead of ":readonly".

* test/lisp/emacs-lisp/cl-lib-tests.el (cl-lib-struct-accessors):
Test using `setf' on read-only accessor to make sure the correct keyword
is used.
---
 test/lisp/emacs-lisp/cl-lib-tests.el | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/test/lisp/emacs-lisp/cl-lib-tests.el b/test/lisp/emacs-lisp/cl-lib-tests.el
index a9c71fa5808..ed7c8b598ea 100644
--- a/test/lisp/emacs-lisp/cl-lib-tests.el
+++ b/test/lisp/emacs-lisp/cl-lib-tests.el
@@ -209,7 +209,7 @@ cl-lib-empty-keyargs
                (:constructor cl-lib--con-1 (&aux (abc 1)))
                (:constructor cl-lib--con-2 (&optional def) "Constructor docstring."))
   "General docstring."
-  (abc 5 :readonly t) (def nil))
+  (abc 5 :read-only t) (def nil))
 (ert-deftest cl-lib-struct-accessors ()
   (let ((x (make-mystruct :abc 1 :def 2)))
     (should (eql (cl-struct-slot-value 'mystruct 'abc x) 1))
@@ -218,8 +218,9 @@ cl-lib-struct-accessors
     (should (eql (cl-struct-slot-value 'mystruct 'def x) -1))
     (should (eql (cl-struct-slot-offset 'mystruct 'abc) 1))
     (should-error (cl-struct-slot-offset 'mystruct 'marypoppins))
+    (should-error (setf (mystruct-abc x) 3))
     (should (pcase (cl-struct-slot-info 'mystruct)
-              (`((cl-tag-slot) (abc 5 :readonly t)
+              (`((cl-tag-slot) (abc 5 :read-only t)
                  (def . ,(or 'nil '(nil))))
                t)))))
 (ert-deftest cl-lib-struct-constructors ()
-- 
2.43.0

From 0fad252490674c3abea8e3ab3f33044e2a5e6551 Mon Sep 17 00:00:00 2001
From: Earl Hyatt <[email-masked]>
Date: Wed, 12 Mar 2025 23:01:49 -0400
Subject: [PATCH v3 2/2] Add cl-with-accessors

* doc/misc/cl.texi (Structures): Mention the new macro.
* lisp/emacs-lisp/cl-macs.el (cl-with-accessors): Add the macro.
* test/lisp/emacs-lisp/cl-lib-tests.el (cl-lib-struct-with-accessors):
Test the macro.
* etc/NEWS (New macro 'cl-with-accessors'.): Mention the macro.

This macro is useful when making repeated use of a structures accessor
functions, such as reading from a slot and then writing to a slot.  It
is similar to 'with-slots' from EIEIO, but uses accessor functions
instead of slot names.
---
 doc/misc/cl.texi                     | 49 ++++++++++++++++++++++++++++
 etc/NEWS                             | 10 ++++++
 lisp/emacs-lisp/cl-macs.el           | 44 +++++++++++++++++++++++++
 test/lisp/emacs-lisp/cl-lib-tests.el | 14 ++++++++
 4 files changed, 117 insertions(+)

diff --git a/doc/misc/cl.texi b/doc/misc/cl.texi
index 48b8ea09c65..e45bf8cb0ca 100644
--- a/doc/misc/cl.texi
+++ b/doc/misc/cl.texi
@@ -4225,6 +4225,55 @@ Structures
 
 Other slot options are currently ignored.
 
+@defmac cl-with-accessors name bindings body@dot{}
+You can use @code{cl-with-accessors} to lexically define symbols as
+expressions calling the given accessor functions on a single instance of
+a structure or class defined by @code{cl-defstruct} or @code{defclass}
+(@pxref{eieio}).  This can simplify code that repeatedly accesses slots.
+With it, you can use @code{setf} and @code{setq} on the symbols like
+normal variables, modifying the values in the structure.  Unlike the
+macro @code{with-slots} (@pxref{Accessing Slots,,,eieio,EIEIO}), because
+the symbol expands to a function call, @code{cl-with-accessors} can be
+used with any generalized variable that can take a single argument, such
+as @code{cl-first} and @code{cl-rest}.
+@end defmac
+
+@example
+;; Using accessors with long, clear names without the macro:
+(defun internal-normalization (person)
+  "Correct the values of the slots in PERSON to be as expected."
+  ;; Check the values of the structure:
+  (when (equal (person-optional-secondary-data person) "")
+    (setf (person-optional-secondary-data person) nil))
+  (when (null (person-access-settings person))
+    (setf (person-access-settings person) 'default))
+  (when (< (long-accessor-name-that-can-become-unreadable-when-repeated
+            person)
+           9)
+    (cl-incf (long-accessor-name-that-can-become-unreadable-when-repeated
+              person)
+             100))
+  ;; And so on before returning the structure:
+  person)
+
+;; Using accessors with long, clear names with the macro:
+(defun internal-normalization (person)
+  "Correct the values of the slots in PERSON to be as expected."
+  (cl-with-accessors ((secondary-data person-optional-secondary-data)
+                      (access-settings person-access-settings)
+                      (short-name person-much-longer-accessor-name))
+      person
+    ;; Check the values of the structure:
+    (when (equal secondary-data "")
+      (setf secondary-data nil))
+    (when (null access-settings)
+      (setf access-settings 'default))
+    (when (< short-name 9)
+      (cl-incf short-name 100))
+    ;; And so on before returning the structure:
+    person))
+@end example
+
 For obscure historical reasons, structure options take a different
 form than slot options.  A structure option is either a keyword
 symbol, or a list beginning with a keyword symbol possibly followed
diff --git a/etc/NEWS b/etc/NEWS
index 31a0c4938ad..66280b8c9ae 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2092,6 +2092,16 @@ that is, buffers not visiting a file and whose names start with a space.
 Previously, such buffers were never shown.  This command is bound to 'I'
 in Buffer Menu mode.
 
+** CL-Lib
+
++++
+*** New macro 'cl-with-accessors'.
+This macro is similar to 'with-slots', but uses accessor functions
+instead of slot names.  It is useful when slots' accessor functions are
+used repeatedly, such as reading from a slot and then writing to that
+slot.  Symbol macros are created for the accessor functions using
+'cl-symbol-macrolet', so that they can be used with 'setq' and 'setf'.
+
 ** Miscellaneous
 
 +++
diff --git a/lisp/emacs-lisp/cl-macs.el b/lisp/emacs-lisp/cl-macs.el
index b07a881ba48..8f1dc7128c2 100644
--- a/lisp/emacs-lisp/cl-macs.el
+++ b/lisp/emacs-lisp/cl-macs.el
@@ -2544,6 +2544,50 @@ cl-once-only
                           collect `(,(car name) ,gensym))
              ,@body)))))
 
+;;;###autoload
+(defmacro cl-with-accessors (bindings instance &rest body)
+  "Use BINDINGS as function calls on INSTANCE inside BODY.
+
+This macro helps when writing code that makes repeated use of the
+accessor functions of a structure or object instance, such as those
+created by `cl-defstruct' and `defclass'.
+
+BINDINGS is a list of (NAME ACCESSOR) pairs.  Inside BODY, NAME is
+treated as the function call (ACCESSOR INSTANCE) using
+`cl-symbol-macrolet'.  NAME can be used with `setf' and `setq' as a
+generalized variable.  Because of how the accessor is used,
+`cl-with-accessors' can be used with any generalized variable that can
+take a single argument, such as `car' and `cdr'.
+
+See also the macro `with-slots' described in the Info
+node `(eieio)Accessing Slots', which is similar, but uses slot names
+instead of accessor functions.
+
+\(fn ((NAME ACCESSOR) ...) INSTANCE &rest BODY)"
+  (declare (debug [(&rest (symbolp symbolp)) form body])
+           (indent 2))
+  (cond ((null body)
+         (macroexp-warn-and-return "`cl-with-accessors' used with empty body"
+                                   nil 'empty-body))
+        ((null bindings)
+         (macroexp-warn-and-return "`cl-with-accessors' used without accessors"
+                                   (macroexp-progn body)
+                                   'suspicious))
+        (t
+         (cl-once-only (instance)
+           (let ((symbol-macros))
+             (dolist (b bindings)
+               (pcase b
+                 (`(,(and (pred symbolp) var)
+                    ,(and (pred symbolp) accessor))
+                  (push `(,var (,accessor ,instance))
+                        symbol-macros))
+                 (_
+                  (error "Malformed `cl-with-accessors' binding: %s" b))))
+             `(cl-symbol-macrolet
+                  ,symbol-macros
+                ,@body))))))
+
 ;;; Multiple values.
 
 ;;;###autoload
diff --git a/test/lisp/emacs-lisp/cl-lib-tests.el b/test/lisp/emacs-lisp/cl-lib-tests.el
index ed7c8b598ea..823569fc18f 100644
--- a/test/lisp/emacs-lisp/cl-lib-tests.el
+++ b/test/lisp/emacs-lisp/cl-lib-tests.el
@@ -228,6 +228,20 @@ cl-lib-struct-constructors
                         (documentation 'cl-lib--con-2 t)))
   (should (mystruct-p (cl-lib--con-1)))
   (should (mystruct-p (cl-lib--con-2))))
+(ert-deftest cl-lib-struct-with-accessors ()
+  (let ((x (make-mystruct :abc 1 :def 2)))
+    (cl-with-accessors ((abc mystruct-abc)
+                        (def mystruct-def))
+        x
+      (should (= abc 1))
+      (should-error (setf abc 99))
+      (should (= def 2))
+      (setf def 3)
+      (should (= def 3))
+      (setq def 4)
+      (should (= def 4)))
+    (should (= 4 (mystruct-def x)))
+    (should (= 1 (mystruct-abc x)))))
 
 (ert-deftest cl-lib-arglist-performance ()
   ;; An `&aux' should not cause lambda's arglist to be turned into an &rest
-- 
2.43.0