I have a pretty good grasp on macros in Lisp, C/C++, TeX, m4, and nroff. Lisp macros are, far and away, the most powerful in that set. In fact, Lisp macros get a bad rap by sharing a name with macros in those other languages. Still, I often hit the same wall when trying to write a Lisp macro so I know that there’s more I have to learn.
A Quick Look at Macros
First, let’s review macros for a moment to make sure we’re all on the same page. The C/C++ macros are the simplest to understand so we’ll start there. The C and C++ compilers run in two stages. First, the C preprocessor runs to expand all of your macros. Then, the compiler runs to actually compile your code. In C and C++, macros are declared using the #define command to the preprocessor. For example, you might do something like the following:
#define END_TIMER __elapsed__ = get_current_time() - __start_time__;
Then, later in you code, you might do something like:
for (ii = 0; ii < MAX_ITERATIONS; ++i) {
do_foo();
}
END_TIMER
The C preprocessor will replace the START_TIMER and END_TIMER with the code you declared in the #define. Your resulting code would then go into the actual compile phase looking like this:
for (ii = 0; ii < MAX_ITERATIONS; ++i) {
do_foo();
}
__elapsed__ = get_current_time() - __start_time__;
The preprocessor did some simple string replacements on your behalf. [Actually, it is somewhat helpful to think of it as doing token replacement instead since the name of your macro has to be a valid C token and you have to jump through some ugly, implementation-specific hoops to get it to concatenate things rather than whitespace separate them.] You can also create macros with arguments like:
...
WARN( "buffer_copy()", "Output buffer is not properly aligned" );
If you look back at the START_TIMER and END_TIMER macros above, you will notice that the semicolons, which delimit statements in C and C++, are included in the macro definitions. As such, they aren’t used explicitly where the macros are invoked. So? Well, that means that your program needn’t bear any resemblance to compilable C before the preprocessor is run. Some sets of macros exploit this to make your C code look like FORTRAN or Bourne Shell or PASCAL or C++.
Lisp macros are a different beast entirely. Invoking a macro in Lisp looks just like everything else in Lisp. [To be fair, some macros (like (LOOP …)) go a little nutty and make things look like a Lisp wrapper around some non-Lisp. Most macros are much more Lispy. Additionally, Lisp has two different sorts of macros: macros and reader macros. With reader macros, you could probably make your Lisp code look something like PASCAL if you were exceedingly ambitious and exceedingly misguided. But, from there on out, when I say macro
, I specifically mean just macro
as distinguished from reader macro
.]
For pedagogic purposes, let’s look at those same macros defined in Lisp:
(defmacro END_TIMER () '(setf *elapsed* (- (get-current-time) *start-time*)))
(defmacro WARN (x y) `(log_message +log-level-warn+ ,x ,y))
The first two macros are straight substitutions: wherever you find the Lisp form (START_TIMER), substitute the form (setf *start-time* (get-current-time)), and similarly for the (END_TIMER) form. The single quote in front of the (SETF …) forms in those first two macros indicates that the following form, while parsed, is not to be evaluated at the time the macro is expanded. In the (WARN …) macro, things are a bit more complicated. The backquote before the (LOG_MESSAGE …) form indicates that the form itself is not to be evaluated at expansion time, but that some portions within it (those demarcated with commas) will need to be substituted. As you would expect:
would expand into:
What makes Lisp macros so great?
From what I’ve shown you so far, you might be thinking: What make Lisp macros so great? Everything you’ve done with Lisp macros, I can do with the C preprocessor. Further, I can do things with the C preprocessor that are syntactically nothing at all like C. You already said you can’t do Lisp macros that are syntactically nothing like Lisp.
There is, of course, an aesthetic argument about why it would be better if the C preprocessor enforced some semblance of C on you. I wouldn’t respect that argument much either if someone were poking at my language. Aesthetics is good. Aesthetics is important. Should the compiler enforce them? Only if it’s going to give me something much better in return.
What do Lisp macros give you? They give you Lisp! What more could you possible want?
Let’s say that you’ve written code in Assembly Language or BASIC so long that the code you’re writing today screams for a Jump table or computed GOTO. Heck, the code you wrote yesterday used a jump table, too? Maybe you could factor out jump tables. You could do this in C by creating a function:
{
(*(array_of_functions[choice]))();
}
...
void (*my_jump_table[])( void ) = { foo, bar, baz, cat, dog, horse, tick };
do_jump_table( choice, my_jump_table );
That will work for you as long it’s okay that all of your functions have the same signature, you don’t mind the overhead of the extra function call to do_jump_table(), you don’t mind making a separate function for each different jump table option, and your choice never stumbles past the end of the jump table. You can do it this way in Lisp if you like, too (without having to write a different do_jump_table for every different return-type you might like from your jump tables.
(funcall (aref array-of-functions choice)))
...
(do-jump-table choice #< #'foo, #'bar, #'baz, #'cat, #'dog, #'horse, #'tick >)
What if you do care about the overhead of the extra function call to do_jump_table(), you do mind making a separate function for each different jump table option, or you are concerned about doing something graceful when your choice stumbles past the end of the jump table? You could address that last concern with more error checking and, in the C case, more parameters to the function. The other concerns are tougher though.
If we don’t want to have to write separate foo, bar, baz, etc. functions, then we want to try this with macros. Rather than taking an array of function pointers, we’re going to let it take some variable number of statements—one for each case in the jump table. Let’s start with the Lisp version:
`(case ,choice
,@(loop for ii from 0
for oo in options
collecting (list ii oo))
(otherwise (do-something-graceful))))
Then, we could use it with some code like this:
(write "NOOP")
(write "SYNC")
(when (>= +protocol-version+ 1)
(write "QUIT")))
This would expand to:
(0 (write "NOOP"))
(1 (write "SYNC"))
(2 (when (>= +protocol-version+ 1)
(write "QUIT")))
(otherwise (do-something-graceful)))
I challenge you to make such a macro in C, C++, TeX, m4, or nroff. I’ve written lots of custom C programs in the past to preprocess my code before sending it to the C preprocessor and the C compiler. In Lisp, it’s all right there. I can use all of Lisp’s magic to turn my code into the code I want it to become. No temporary files. No fancy Makefile tricks to get everything squared away. It’s just all there.
What don’t I know?
The do-jump-table macro we just defined expands to one statement (one lisp form): (CASE …). There are many times when I want my macro to expand to more than one form, however. This isn’t legit. I still end up wanting to do it. For example, I might think that since I’m doing things with macros, that I might want the whole (CASE …) construct to be in the main code and just have the macro expand the different cases:
(do-jump-table-cases (write "NOOP")
(write "SYNC")
(when (>= +protocol-version+ 1)
(write "QUIT")))
(otherwise (do-something-especially-graceful)))
Lisp says no. I can understand why it says that. In this case, it’s easy enough to work around. But, in other cases, I end up flailing.
I feel that once I understand Lisp macros more completely, I won’t keep trying to jam that gorgeous, round peg into all of these square holes in my logic.
While your experimenting with macros you might be interested in checking out the concatenative macro processor I’ve been working on, it’s an unusual take the topic:
http://freshmeat.net/projects/minimac-macro-processor
Cheers,
Mark H.
That’s interesting. I may have to tinker with it next time I’m thinking of using m4 for something. I’ll have to look more closely, too. I didn’t see any examples of macros with arguments.
It’s a concatenative macro processor, arguments are taken from an explicit stack. Here’s an example:
Example 2: 99 bottles of beer (http://99-bottles-of-beer.net/)
`drink! ( u — ) `{continue}${#}
this-many-bottles of beer on the wall, this-many-bottles of beer.
Take one down and pass it around, fewer-bottles of beer on the wall.
{repeat}`
`oh-no! ( — ) `No more bottles of beer on the wall, no more bottles of beer.`
`please! ( u — u ) `{#}
Go to the store and buy some more, this-many-bottles of beer on the wall.`
`this-many-bottles ( u — u ) `[{dup} 0& ~no more bottles~1& ~1 bottle~{.} ~ bottles~]`
`fewer-bottles ( u — u-1 ) `{–}$this-many-bottles`
99& drink!
oh-no!
99& please!
If you want more than one form, just wrap them all in PROGN. It’s defined to preserve toplevelness, if that’s your concern, so the following will work as you’d expect it to when placed at the toplevel:
(defclass ...)
(defun ...)
(defvar ...))
That doesn’t help me in my situation. In my situation, the enclosing macro tries to make some determination based on the number of forms it contains. If one of those forms is a (progn …), it will look like only one form instead of however many it contains.
In other situations, I want to splice something into the middle of a list… not into the middle of code.
[…] few weeks back, I posted about how I know that I have more to grok about Lisp macros. In that post, I needed to explain Lisp macros a bit for those not in the know. The example that I […]
well, you could use some reader macros to use a modified do-jump-table-cases
`(list ,@(loop for ii from 0
for oo in options
collecting `'(,ii ,oo)))))
like this:
#.`(ecase op-code
,@(do-jump-table-cases
(write "NOOP")
(write "SYNC")
(write "QUIT"))))
which generates (at read time)
(ecase op-code
(0 (write "NOOP"))
(1 (write "SYNC"))
(2 (write "QUIT"))))
doesn’t look so nice though 😉