← Articles
Published , 4 minute read
In a previous post, I professed my fondness for the LOOP macro and introduced something like its collect
keyword delivered as a function called COLLECTOR
.
COLLECTOR
was cool. Maybe the coolest thing about it was that the function returned by COLLECTOR
could be passed as an argument to other functions. But that’s a pretty obscure affordance that I think is overshadowed by two unfortunate characteristics:
FUNCALL
The syntax problem is subjective. Maybe you don’t mind seeing FUNCALL
here and there. The performance problem is also subjective; your Lisp implementation might inline the collecting function, or you just might not care.
But my treatment of the problem wouldn’t be complete without suggesting an approach that addressed the potential syntax and performance problems. That approach, of course, is the one involving macros.
COLLECTOR
had a functional API, in the sense that it returned a function. As a return value, the returned function could not be called like a conventional Lisp operator; FUNCALL
was necessary.
With macros, however, it’s possible to support operator syntax. Here’s what I have in mind:
WITH-COLLECT
is the name of the macro. Its first argument — COLLECT
— is a symbol, the name to use for the collecting operator within the context of the body.COLLECT
is called like a function.COLLECT
with no arguments to return the list head, WITH-COLLECT
evaluates to the head of the collected list.(defmacro with-collect (collect-name &body body)
(let ((head (gensym "head"))
(tail (gensym "tail")))
`(let ((,head nil)
(,tail nil))
(macrolet ((,collect-name (item)
`(cond
((null ,',tail)
(setq ,',tail (cons ,item nil)
,',head ,',tail))
(t (let ((new-tail (cons ,item nil)))
(setf (cdr ,',tail) new-tail
,',tail new-tail))))))
,@body
,head))))
Like COLLECTOR
, code for maintaining the tail of the list is in there. Unlike COLLECTOR
, that code now lives in a macro instead of a function.
In particular, the operative code is inside the body of a local macro defined by MACROLET
. Like a normal macro defined via DEFMACRO
, MACROLET
is a way to replace occurrences of operator calls with arbitrary code. But unlike DEFMACRO
, definitions created by MACROLET
are local and not global: they are only usable within the specified lexical extent.
The pickle with WITH-COLLECT
is we needed to parameterize the name of the macro defined by MACROLET
— we wanted to give the user the ability to pick their own name for the collecting operator.
We also need to parameterize the body of the MACROLET
; we want the user to supply the code within which the MACROLET
will apply.
The way to parameterize any syntax, even that of MACROLET
, is with a macro. So, we wrap everything in a DEFMACRO
that accepts the COLLECT-NAME
and BODY
arguments.
One thing that might stick out to you is the peculiar ,',
syntax. What’s happening is, the presence of two backticks (one macro is producing another) makes two levels of unquoting necessary. Since the innermost unquote yields a symbol, it must be wrapped in a quote to prevent its evaluation as a value.
I think it’s fair to say that we simplified the API, because the entire expression now evaluates to the collected list. The collection operation now also looks like a function call and is not obscured by FUNCALL
.
Code size is larger, because every occurrence of the user-named collecting operator now expands to the collection operation code instead of being a function call.
I like it though. All things considered, I think I generally prefer something like WITH-COLLECT
to COLLECTING
.
There’s a COLLECTING entry on CLiki with a bunch of pointers to other macro implementations.
The Code-Walking with Macrolet section of Chapter 5 of Let Over Lambda is a nice treatment of MACROLET
. As far as I can tell, the most common use of MACROLET
is inside DEFMACRO
s in order to support a global macro’s operator semantics.