Macro that defines functions whose names are based on the macro's arguments
Asked Answered
H

2

6

*Note: Despite having frequented StackOverflow for a long time, this is the first question that I have posted myself. Apologies if it's a bit verbose. Constructive criticism appreciated.

When I define a struct in Common Lisp using defstruct, a predicate function is automatically generated that tests whether its argument is of the type defined by the defstruct. Eg:

(defstruct book
  title
  author)

(let ((huck-finn (make-book :title "The Adventures of Huckleberry Finn" :author "Mark Twain")))
  (book-p huck-finn))
=> True

However, when defining a class using defclass, such functions are seemingly not generated by default (is there a way to specify this?), so I'm trying to add this functionality myself, because I'd like a) for this syntax to be consistent between structs and classes, b) to have an abbreviation of (typep obj 'classname), which I need to write very often and is visually noisy, and c) as a programming exercise, since I'm still relatively new to Lisp.

I could write a macro that defines a predicate function given the name of a class:

(defclass book ()
  ((title :initarg :title
          :accessor title)
   (author :initarg :author
           :accessor author)))

;This...
(defmacro gen-predicate (classname)
  ...)

;...should expand to this...
(defun book-p (obj)
  (typep obj 'book))

;...when called like this:
(gen-predicate 'book)

The name that I need to pass to defun must be of the form 'classname-p. Here's where I have difficulty. To create such a symbol, I could use the "symb" function from Paul Graham's On Lisp (p. 58). When it is run on the REPL:

(symb 'book '-p)
=> BOOK-P

My gen-predicate macro looks like this so far:

(defmacro gen-predicate (classname)
  `(defun ,(symb classname '-p) (obj)
     (typep obj ,classname)))

(macroexpand `(gen-predicate 'book))
=>
(PROGN
 (EVAL-WHEN (:COMPILE-TOPLEVEL) (SB-C:%COMPILER-DEFUN '|'BOOK-P| 'NIL T))
 (SB-IMPL::%DEFUN '|'BOOK-P|
                  (SB-INT:NAMED-LAMBDA |'BOOK-P|
                      (OBJ)
                    (BLOCK |'BOOK-P| (TYPEP OBJ 'BOOK)))
                  NIL 'NIL (SB-C:SOURCE-LOCATION)))
T

It would seem that the symbol created by (symb 'book '-p) is actually considered |'BOOK-P| by the implementation (SBCL), not BOOK-P. Sure enough, this now works:

(let ((huck-finn (make-instance 'book)))
  (|'BOOK-P| huck-finn))
=> True

Why is the symbol created by symb interned as |'BOOK-P|? In On Lisp (same page as above) Graham says: "Any string can be the print-name of a symbol, even a string containing lowercase letters or macro characters like parentheses. When a symbol's name contains such oddities, it is printed within vertical bars." No such oddities exist in this case, do they? And am I correct in thinking that the "print-name" of a symbol is what is actually displayed on the standard output when the symbol is printed, and is, in the case of such oddities, distinct from the form of the symbol itself?

To be able to write function-defining macros like gen-predicate - whose defined functions are named based on the arguments passed to the macro - seems to me like something that Lisp hackers have probably been doing for ages. User Kaz says here (Merging symbols in common lisp) that the "mashing-up" of symbols can often be avoided, but that would defeat the purpose of this macro.

Finally, assuming I could get gen-predicate to work how I want, what would be the best way of ensuring that it be called for each new class as they are defined? Much in the same way as initialize-instance can be customized to perform certain actions upon instantiation of a class, is there a generic function called by defclass that can perform actions upon definition of a class?

Thank you.

Helminthiasis answered 23/5, 2015 at 17:14 Comment(0)
P
5

That's a usual problem: what gets passed to a Macro?

Compare calls like this:

(symb 'book '-p)

and

(symb ''book '-p)

Your macro form is this:

(gen-predicate 'book)

GEN-PREDICATE is a macro. classname is a parameter for this macro.

Now what is the value of classname inside the macro during code expansion? Is it book or 'book?

Actually it is the latter, because you wrote (gen-predicate 'book). Remember: macros see source code and the argument source gets passed to the macro function - not the value. The argument is 'book. Thus this gets passed. (QUOTE BOOK) is the same, only printed differently. So it is a two element list. The first element is the symbol QUOTE and the second element is the symbol BOOK.

Thus the macro now calls the function SYMB with the argument value (QUOTE BOOK) or, shorter, 'BOOK.

If you want to generate the predicate without the quote character, you need to write:

(gen-predicate book)

Alternatively you can also change the macro:

(symb classname '-p)

would be:

(symbol (if (and (consp classname)
                 (eq (first classname) 'quote))
           (second classname)
           classname))

Compare

We write

(defun foo () 'bar)

and not

(defun 'foo () 'bar)    ; note the quoted FOO

DEFUN is a macro and the first argument is the function name. It's a similar problem then...

Second part of the question

I don't really know any good answer to that. I can't remember any easy way to run code (for example to define a function) after a class definition.

  • Maybe use the MOP, but that's ugly.

  • write a custom macro DEFINE-CLASS which does what you want: expands into DEFCLASS and the DEFUN.

  • iterate over all symbols in a package, find the classes and define the corresponding predicates

Pacifistic answered 23/5, 2015 at 18:34 Comment(2)
Of course, brilliant! I somehow didn't consider that 'book is really (quote book). I understand your second solution, but the first is confusing. I had actually already realised that (macroexpand (gen-predicate book) expands into what I wanted, but since the symbol book` is not quoted, then it is interpreted as a variable, which is undefined. I didn't know how to avoid that problem. Thank you for your prompt and helpful reply!Helminthiasis
This answer does a nice job of getting at the first part of the question. There's a second part, too, that I'd be curious to see addressed: how to get this to always be called when new classes are created? (And/or when would or wouldn't one want to do that?)Gilbertine
J
1

To address the second part of the question, classes are themselves objects, thanks to the MOP, so it might be possible to write an :after method on initialize-instance specialized on STANDARD-CLASS. But you should check the MOP to see whether defining such a method is allowed or not.

If it's possible, then yes, you can run code in response to the creation of a class; however, since you don't know the name of the class being created until runtime, you cannot spell it textually in the source, so you cannot use your macro (unless you use eval). You'd rather use something like

(let ((classname (class-name class)))
  (compile (generate-my-predicate-symbol classname)
    (lambda (x) (typep x classname))))

I think Rainer's suggestion to write your own DEFINE-CLASS macro is the way to go, I mean, the way a seasoned Lisper most likely would do it, if there aren't any other considerations at play. But I'm not really a seasoned Lisper, so I might be wrong ;)

Jacobin answered 25/5, 2015 at 9:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.