Manfred
Manfred

Reputation: 517

Defining class and methods in macro

I'm still quite new to Common Lisp macros.

For an abstraction over a defclass with defgeneric I thought it'd be nice to make a macro.

A complitely naive implementation looks like:

(defmacro defgserver (name &key call-handler cast-handler)
  "TODO: needs firther testing. Convenience macro to more easily create a new `gserver' class."
  `(progn
     (defclass ,name (gserver) ())
     (defmethod handle-call ((server ,name) message current-state)
       ,(if call-handler call-handler nil))
     (defmethod handle-cast ((server ,name) message current-state)
       ,(if cast-handler cast-handler nil))))

When used the error says that 'message' is not known. I'm not sure. 'message' is the name of a parameter of defgeneric:

(defgeneric handle-call (gserver message current-state))

Using the macro I see a warning 'undefined variable message':

(defgserver foo :call-handler 
           (progn
             (print message)))
; in: DEFGSERVER FOO
;     (PRINT MESSAGE)
; 
; caught WARNING:
;   undefined variable: COMMON-LISP-USER::MESSAGE

Which when used has this consequence:

CL-USER> (defvar *my* (make-instance 'foo))
*MY*
CL-USER> (call *my* "Foo")
 <WARN> [10:55:10] cl-gserver gserver.lisp (handle-message fun5) -
  Error condition was raised on message processing: CL-GSERVER::C: #<UNBOUND-VARIABLE MESSAGE {1002E24553}>

So something has to happen with message and/or current-state. Should they be interned into the current package where the macro is used?

Manfred

Upvotes: 1

Views: 239

Answers (1)

user5920214
user5920214

Reputation:

The problem, as mentioned, is that you are talking about different symbols.

However this is really a symptom of a more general problem: what you are trying to do is a sort of anaphora. If you fixed up the package structure so this worked:

(defgserver foo :call-handler 
           (progn
             (print message)))

Then, well, what exactly is message? Where did it come from, what other bindings exist in that scope? Anaphora can be useful, but it also can be a source of obscure bugs like this.

So, I think a better way to do this, which avoids this problem is to say that the *-handler options should specify what arguments they expect. So instead of the above form you'd write something like this:

(defgserver foo
  :call-handler ((server message state)
                 (print message)
                 (detonate server)))

So here, value of the :call-handler-option is the argument list and body of a function, which the macro will turn into a method specialising on the first argument. Because the methods it creates have argument lists provided by the user of the macro there's never a problem with names, and there is no anaphora.

So, one way to do that is to do two things:

  • make the default values of these options be suitable for processing into methods without any special casing;
  • write a little local function in the macro which turns one of these specifications into a suitable (defmethod ...) form.

The second part is optional of course, but it saves a little bit of code.

In addition to this I've also done a slightly dirty trick: I've changed the macro definition so it has an &body option, the value of which is ignored. The only reason I've done this is to help my editor indent it better.

So, here's a revised version:

(defmacro defgserver (name &body forms &key 
                           (call-handler '((server message current-state)
                                           (declare (ignorable 
                                                     server message current-state))
                                           nil))
                           (cast-handler '((server message current-state)
                                           (declare (ignorable 
                                                     server message current-state))
                                           nil)))
  "TODO: needs firther testing. Convenience macro to more easily
create a new `gserver' class."
  (declare (ignorable forms))
  (flet ((write-method (mname mform)
           (destructuring-bind (args &body decls/forms) mform
             `(defmethod ,mname ((,(first args) ,name) ,@(rest args))
               ,@decls/forms))))
    `(progn
       (defclass ,name (gserver) ())
       ,(write-method 'handle-call call-handler)
       ,(write-method 'handle-cast cast-handler))))

And now

(defgserver foo
  :call-handler ((server message state)
                 (print message)
                 (detonate server)))

Expands to

(progn
  (defclass foo (gserver) nil)
  (defmethod handle-call ((server foo) message state)
    (print message)
    (detonate server))
  (defmethod handle-cast ((server foo) message current-state)
    (declare (ignorable server message current-state))
    nil))

Upvotes: 2

Related Questions