Search code examples
lispcommon-lisp

Trying to make macro similar to defparameter, defvar but macro only returns an s-expression


I'm trying to make macros for defining various object similar to defparameter and defvar. The defregion1 macro works: upon executing it defines a variable with object region. However, defregion2 only returns an expression that must be executed manually. Here is the code:

(defclass location ()
  ((x
    :initarg :x
    :accessor x)
   (y
    :initarg :y
    :accessor y)))

(defclass region ()
  ((x :initarg :x
      :accessor x)
   (y :initarg :y
      :accessor y)
   (w :initarg :w
      :accessor w)
   (h :initarg :h
      :accessor h)))

(defmacro deflocation (var x y)
  `(defparameter ,var `(make-instance 'location :x ,x :y ,y)))


(defmacro defregion1 (var x y w h)
  `(defparameter ,(intern (symbol-name var))
       (make-instance 'region :x ,x :y ,y :w ,w :h ,h)))

(defmacro defregion2 (var l1 l2)
  `(with-slots ((x1 x) (y1 y))
       ,l1
     (with-slots ((x2 x) (y2 y))
         ,l2
       `(defparameter ,(intern (symbol-name ,var))
           (make-instance 'region
                          :x ,x1 :y ,y1 :w (- ,x2 ,x1) :h (- ,y2 ,y1))))))

The output of defregion1:

(defregion1 *test-reg1* 1 2 3 4) 

=> *test-reg1*

The output of deferegion2:

(deflocation *l1* 20 30)
(deflocation *l2* 50 60)
(defregion2 '*test-reg2* *l1* *l2*)

=> (DEFPARAMETER *TEST-REG2*
     (MAKE-INSTANCE 'REGION :X 20 :Y 30 :W (- 50 20) :H (- 60 30)))

I want *test-reg2* to also become a variable. What is wrong here?


Solution

  • You have two nested backquotes.

    But your macro is also inside-out: you really want defparameter at the top-level, so something like this would be better:

    (defmacro defregion2 (var l1 l2)
      `(defparameter ,(intern (symbol-name ,var)) 
         (with-slots ((x1 x) (y1 y))
             ,l1
           (with-slots ((x2 x) (y2 y))
               ,l2
             (make-instance 'region :x x1 :y y1 :w (- x2 x1) :h (- y2 y1))))))
    

    Also are you sure you want this slightly odd internery? What that's going to do is take the name of the symbol you give as an argument and intern it in the current package. So for instance

    (defregion2 x:*foo* ...)
    

    will result in a symbol *foo* in the current package, instead of giving a value to x:*foo*. (Of course this all collapses into the same thing if the current package is x).

    I suspect you possibly want

    (defmacro defregion2 (var l1 l2)
      `(defparameter ,var
         (with-slots ((x1 x) (y1 y))
             ,l1
           (with-slots ((x2 x) (y2 y))
               ,l2
             (make-instance 'region :x x1 :y y1 :w (- x2 x1) :h (- y2 y1))))))
    

    Your code is also potentially unhygenic as it binds variables (really symbol macros) with names which are visible to whatever l2 is: it would be safer as

    (defmacro defregion2 (var l1 l2)
      `(defparameter ,var
         (let ((l1 ,l1) (l2 ,l2))
           (with-slots ((x1 x) (y1 y))
               l1
             (with-slots ((x2 x) (y2 y))
                 l2
               (make-instance 'region :x x1 :y y1 :w (- x2 x1) :h (- y2 y1)))))))
    

    This is now safe as you can see from the expansion:

    (defregion2 *thing* 
                (expression-involving x1 x2)
                (another-expression-involving x1 x2))
    

    expands to

    (defparameter *thing*
      (let ((l1 (expression-involving x1 x2))
            (l2 (another-expression-involving x1 x2)))
        (with-slots ((x1 x) (x2 y)) l1
          (with-slots ((x2 x) (y2 y)) l2
            (make-instance 'region :x x1 :y y1 :w (- x2 x1) :h (- y2 y1))))))
    

    You can see that the x1 in (another-expression-involving x1 ...) is not the one that is bound by with-slots.