Search code examples
macroscommon-lispsymbolsclosmalformed

Why this symbol expasion is being malformed in Common Lisp?


I am trying to do the exercises on this tutorial about CLOS using SBCL and Slime (Emacs).

I have this class, instance, and function to set values for the slots:

(defclass point ()
  (x y z))

(defvar my-point
  (make-instance 'point))

(defun with-slots-set-point-values (point a b c)
  (with-slots (x y z) point (setf x a y b z c)))

Using the REPL, it works fine:

CL-USER> (with-slots-set-point-values my-point 111 222 333)

333

CL-USER> (describe my-point)
#<POINT {1003747793}>
  [standard-object]

Slots with :INSTANCE allocation:
  X                              = 111
  Y                              = 222
  Z                              = 333
; No value

Now, the exercises indicates that using the symbol-macrolet I need to implement my version of with-slots.

I have a partial implementation of my with-slots (I still need to insert add the operation):

(defun partial-my-with-slots (slot-list object)
   (mapcar #'(lambda (alpha beta) (list alpha beta))
           slot-list
           (mapcar #'(lambda (var) (slot-value object var)) slot-list)))

It works when calling it:

CL-USER> (partial-my-with-slots '(x y z) my-point)
((X 111) (Y 222) (Z 333))

Since this use of symbol-macrolet works:

CL-USER> (symbol-macrolet ((x 111) (y 222) (z 333))
   (+ x y z))
666

I tried doing:

CL-USER> (symbol-macrolet (partial-my-with-slots '(x y z) my-point)
   (+ x y z))

But, for some reason that I do not know, Slime throws the error:

malformed symbol/expansion pair: PARTIAL-MY-WITH-SLOTS
   [Condition of type SB-INT:SIMPLE-PROGRAM-ERROR]

Why does this happen? How can I fix this?


Solution

  • You can't write with-slots as a function which is called at run time. Instead it needs to be a function which takes source code as an argument and returns other source code. In particular if given this argument

    (my-with-slots (x ...) <something> <form> ...)
    

    It should return this result:

    (let ((<invisible-variable> <something))
      (symbol-macrolet ((x (slot-value <invisible-variable>)) ...)
        <form> ...))
    

    You need <invisible-variable> so you evaluate <object-form> only once.

    Well, here is a function which does most of that:

    (defun mws-expander (form)
      (destructuring-bind (mws (&rest slot-names) object-form &rest forms) form
        (declare (ignore mws))
        `(let ((<invisible-variable> ,object-form))
           (symbol-macrolet ,(mapcar (lambda (slot-name)
                                        `(,slot-name (slot-value <invisible-variable> 
                                                                 ',slot-name)))
                                      slot-names)
             ,@forms))))
    

    And you can check this:

    > (mws-expander '(my-with-slots (x y) a (list x y)))
    (let ((<invisible-variable> a))
      (symbol-macrolet ((x (slot-value <invisible-variable> 'x))
                        (y (slot-value <invisible-variable> 'y)))
        (list x y)))
    

    So that's almost right, except the invisible variable really needs to be invisible:

    (defun mws-expander (form)
      (destructuring-bind (mws (&rest slot-names) object-form &rest forms) form
        (declare (ignore mws))
        (let ((<invisible-variable> (gensym)))
          `(let ((,<invisible-variable> ,object-form))
             (symbol-macrolet ,(mapcar (lambda (slot-name)
                                         `(,slot-name (slot-value ,<invisible-variable>
                                                                  ',slot-name)))
                                       slot-names)
             ,@forms)))))
    

    And now:

    > (mws-expander '(my-with-slots (x y) a (list x y)))
    (let ((#:g1509 a))
      (symbol-macrolet ((x (slot-value #:g1509 'x))
                        (y (slot-value #:g1509 'y)))
        (list x y)))
    

    Well, a function which takes source code as an argument and returns other source code is a macro. So, finally, we need to install this function as a macroexpander, arranging to ignore the second argument that macro functions get:

    (setf (macro-function 'mws)
          (lambda (form environment)
            (declare (ignore environment))
            (mws-expander form)))
    

    And now:

    > (macroexpand '(mws (x y) a (list x y)))
    (let ((#:g1434 a))
      (symbol-macrolet ((x (slot-value #:g1434 'x)) (y (slot-value #:g1434 'y)))
        (list x y)))
    

    This would be more conventionally written using defmacro, of course:

    (defmacro mws ((&rest slot-names) object-form &rest forms)
      (let ((<invisible-variable> (gensym)))
        `(let ((,<invisible-variable> ,object-form))
           (symbol-macrolet ,(mapcar (lambda (slot-name)
                                       `(,slot-name (slot-value ,<invisible-variable> ',slot-name)))
                                     slot-names)
             ,@forms))))
    

    However the two definitions are equivalent (modulo needing some eval-whenery to make the first work properly with the compiler).