tips for writing common lisp macros
use loop⌗
loop
is one of the most useful functions one could use for sequences. It can do a bunch of things:
- It starts from a number and ends in a number
- It can range over a list, vector, hash-table
- It can
collect
orappend
values through each iteration to put them into a list - It can define variables at the start of a loop
There are videos online that go much deeper on how powerful loop
s are. But, you want to use them in macros because macros return lists and loop
s are the perfect tool to return lists and subsequently, code forms. For example, I recently wanted to generate constants for a protocol I am trying to parse.
The protocol’s constants start from a certain number and end with a certain number. But, each constant has two versions a Tconstant
and Rconstant
. So, effectively if the value of constant was 1
, Tconstant
would be 1
, Rconstant
would be 2
. Tconstant2
would be 3
and so on.
Here’s how I used loop to define those constants:
(defun rfmt (&rest args)
(read-from-string (apply #'fmt `(nil ,@args))))
(defparameter *messages-enum*
(loop
:for x :in
`(:openfd :version :auth :attach :error :flush :walk :open :create :read :write :clunk :remove :stat :wstat)
:for i :from 98 :by 2
:append (list (rfmt ":t~a" x) i (rfmt ":r~a" x) (1+ i))))
That loop
expression generates a list
that looks like this:
(:topenfd 98 :ropenfd 99 :tversion 100 :rversion 101)
And so on. Cool, ain’t it?
mapcar⌗
When loop
is only used for collect
ing a function form for a list, use mapcar
.
(loop for x in list collect (+ 5 x))
;; use this
(mapcar (lambda (x) (+ 5 x)) list)
Although the difference isn’t drastic, mapcar
is better etiqutte. Both produce the same result. I sometimes use loop
’s do
in-order to set elements in a vector
or string
like so:
(loop for i from 0 below (length list) do (setf (elt list i) (+ 5 (elt list i))))
Which is quite verbose but it gets the job done across vectors, arrays, strings(which are arrays) as well as lists too.
use tiny functions once the macro gets too big⌗
Macros get bloated way too soon. My way to combat this was to create little functions that return lists that resemble code. For example, if I had a macro that generated a function and I needed to declare
the types of my arguments, I’d just define a function like so:
(defun declare-for-my-macro (arg)
(case arg
(number 'integer)
(string 'string)
(t 't)))
Then use it in a macro like so:
(defun collect-declare-forms (arg)
`(,(declare-for-my-macro arg) ,arg))
(defmacro my-macro ()
`(defun some-random-function (string number other)
(declare ,(mapcar #'collect-declare-forms `(string number other)))
(print (format nil "~a ~a ~a" string number other))))
Which returns:
(defun some-random-function (string number other)
(declare ((string string) (integer number) (t other)))
(print (format nil "~a ~a ~a" string number other)))
macroexpand-1 is there for you⌗
macroexpand-1
singlehandedly carries all the macros in Common Lisp on its back and can carry more. What this neat little function does is it expands the macro without evaluating it.
So, imagine you writing a macro and you are having issues with its expansion. You could always invoke macroexpand-1
to see what the result of your macro is before evaluating it:
;; from the macro defined above
;; the return value is the same as the one above
(macroexpand-1 `(my-macro))
(macroexpand-1 `(loop repeat 5 collect (random 5)))
You can also use macroexpand
which expands ALL macros in your macro. For example, if you had a loop within the return value of a macro, that gets evaluated and what is returned is the final result of a macro.