Sunday, March 23, 2008

Macro Magic

This is a neat trick that I discovered the other day while mucking around in Lisp Land, and I figured I'd share it with my fellow Common Lisp programmers:

Now, we all know how macros can allow us to seamlessly extend Lisp so that our own code looks the same as the language primitives. However, not everyone knows just how seamless these extensions can get.

We're going to develop a series of macros that allow us to determine how many arguments a function takes. Our goal is to be able to run the following code:

(defun test (a b)
(+ a b))

(num-args test)


And have num-args correctly report 2 as the number of arguments for test.

Obviously, our first step should be to get the definition of defun from the common-lisp package out of our way so we can create our own. This is done by creating a new package:

(defpackage :test
(:use :common-lisp)
(:shadow :defun))

(in-package :test)

The only thing special about this package definition is that it shadows the defun symbol, so it is undefined in our test package. Fortunately, we'll still be able to access the symbol in the common-lisp package when we need it.

We're going to need a place to store the argument list for each function that is defined in our new package. We'll be using a hash table for this purpose:

(defvar *arg-lists* (make-hash-table))

Now we get to the main event: the actual defun macro:

(defmacro defun (name varlist &rest body)
`(progn
(common-lisp:defun ,name ,varlist ,@body)
(setf (gethash ',name *arg-lists*) ',varlist)))

Despite being a short macro, it manages to get a lot of work done. Not only does it define a function just like a normal defun would, but it also stores the function's argument list in our hash table. The trick is in calling the common-lisp package's version of defun while simultaneously defining our own.

Now all that's left to do is provide a way of retrieving a function's argument list and finding its length. We'll do this with another macro:

(defmacro num-args (name)
`(length (gethash ',name *arg-lists*)))

Finally, you can evaluate the code at the beginning at get the correct response of 2:

(defun test (a b)
(+ a b))

(num-args test)

So there you have it. A normally complex bit of introspection added to Common Lisp in 11 lines of code, thanks to our trusty macros. I'll leave it as an exercise to the reader to extend this program: it can't determine how many arguments a macro takes, and it counts special words in the argument list like &rest as arguments.

3 comments:

J.V. Toups said...

Seems like it would be better to write a generic wrapper around the the actual function/lambda implementation in whatever CLs you want to support so that you can get the arg-list of functions defined before/outside of the scope where your special defun is defined.

I wonder how hard that would be. I really don't know much about the implementation details of most Cls - but I am sure the information is there.

Ben said...

noob question: Why use a macro for num-args instead of a function?

terry said...

thats great. i wondered what shadow was for.
something like that ive always wanted to do to mapcar => into => (map fn list).
(defpackage :test (:use #:cl) (:shadow #:defun)).
(defpackage "TEST" (:use "COMMON-LISP") (:shadow "DEFUN")).
only just got to grips with defpackage myself. there might be potential problem of interning symbols by the reader if just use :common-lisp ?
dont know , am only guessing. i dont know enough yet.