ooplispcommon-lispcloslisp-macros

Common Lisp - Any ideas on how to write a "with-methods" macro similar to "with-slots"?


So my idea is to reduce the amount of calls to an object instance, when it is clearly the only one in scope.
Here's a code example:

(defun read-plist (plist)
    (let ((plist-reader (make-instance 'plist-reader))
        (loop for (indicator value) in plist
            do (read-indicator plist-reader indicator)
            do (read-value plist-readere value))
        plist-reader)) 

And here is what I want it to look like:

(defun read-plist (plist)
    (let ((plist-reader (make-instance 'plist-reader))
        (with-methods (read-indicator (rv read-value)
                plist-reader)
            (loop for (indicator value) in plist
                do (read-indicator indicator)
                do (rv value)))
        plist-reader))

I suppose it's not really the best example since it makes the code look even more complicated. But I can imagine a case where it would shave off a good bit of complexity. Maybe I'm looking in a totally wrong direction, but my original idea is to reduce the use of names, when scope is clearly local. Would it simply be a better practice to have a small package of its own, where functions replace the need to bind methods to a specific object? I suppose there isn't one true answer, but anyways.. What are your takes on the macro? :D

I tried looking in the direction of defining the macro this way:

(defmacro with-methods (methods class-instance &rest code) 
    ...) 

But it seems I would need to look through all the rest code, figure find all the occurences of the given methods and modify them as lists. It raises a dilema of performance issues to me. Maybe there's a classical approach to this that I have missed? From a standpoint of a Common Lisp beginner, I find documentation unsearchable and only readable after ChatGPT's take on it tbh.


Solution

  • I'm not at all sure that doing this is something that's desirable at all. However, if it is there are two obvious problems:

    These problems, between them, make the problem extremely hard to solve in any general way.

    The first one can be avoided by simply insisting that the additional argument you're going to insert is the first. That leaves the second. The second can be avoided by insisting that there are no non-simple argument lists, and in particular no lambda-list keywords in the argument list. These simplifications make the resulting simpler version of the problem tractable. It's still not clear to me that it's desirable.

    The following macro specifies method shorthands as (shorthand method/args) where method/args is either a method name or a cons of (method-name . extra-args). It avoids using apply, inlines the local functions and evaluates the object form only once.

    (defmacro with-method-shorthands ((&rest method-specifiers) object &body forms)
      ;; All methods take the object as their first argument.  There can
      ;; be no non-simple arglists for method shorthands.
      (let ((<object> (make-symbol "<OBJECT>")))
        `(let ((,<object> ,object))         ;with-names
           (flet ,(mapcar (lambda (method-specifier)
                            (destructuring-bind (name method/args) method-specifier
                              (multiple-value-bind (method additional-args)
                                  (etypecase method/args
                                    (symbol
                                     (values method/args '()))
                                    (cons
                                     (values (first method/args) (rest method/args))))
                                (unless (null (intersection additional-args lambda-list-keywords))
                                  (error "Can't parse complicated arglist"))
                                `(,name (,@additional-args)
                                   (,method ,<object> ,@additional-args)))))
                          method-specifiers)
             (declare (inline ,@(mapcar #'first method-specifiers)))
             ,@forms))))
    

    Now

    (with-method-shorthands ((foo (bar a)) (bine bone)) (thing-returning-object)
      (foo x))
     -> (let ((#:<object> (thing-returning-object)))
          (flet ((foo (a) (bar #:<object> a)) (bine () (bone #:<object>)))
            (declare (inline foo bine))
            (foo x)))
    

    Where all the symbols that look the same are the same. Note this is not even slightly going to work on setf methods, which very often specialise on their second argument:

    (defmethod (setf thing) (new (object my-class))
      ...)
    

    For fun, here is a more complicated version of the above macro which allows you to specify where you want the object to be spliced in the arguments:

    (defmacro with-method-shorthands ((&rest method-specifiers) object &body forms)
      (let ((<object> (make-symbol "<OBJECT>")))
        `(let ((,<object> ,object))         ;with-names
           (flet ,(mapcar (lambda (method-specifier)
                            (destructuring-bind (name method/args) method-specifier
                              (multiple-value-bind (method pre-args post-args)
                                  (etypecase method/args
                                    (symbol
                                     (values method/args '() '()))
                                    (cons
                                     (destructuring-bind (method &rest options) method/args
                                       (unless (null (intersection options lambda-list-keywords))
                                         (error "complicated arglist"))
                                       (cond
                                        ((some #'keywordp options)
                                         (destructuring-bind (&key (pre '()) (post '())) options
                                           (values method pre post)))
                                        ((every #'symbolp options)
                                         (values method '() options))
                                        (t
                                         (error "bogus arglist (non-symbols)"))))))
                                `(,name ,(append pre-args post-args)
                                   (,method ,@pre-args ,<object> ,@post-args)))))
                          method-specifiers)
             (declare (inline ,@(mapcar #'first method-specifiers)))
             ,@forms))))
    

    With this version

    (with-method-shorthands ((a (a :pre (x y) :post (z)))
                             (b (b p q))
                             (c c))
                            (make-thing)
      (a b c d))
     -> (let ((#:<object> (make-thing)))
          (flet ((a (x y z)
                   (a x y #:<object> z))
                 (b (p q)
                   (b #:<object> p q))
                 (c ()
                   (c #:<object>)))
            (declare (inline a b c))
            (a b c d)))