Metatronic macros

:: programming, lisp

Metatronic macros are a simple hack which makes it a little easier to write less unhygienic macros in Common Lisp.

Common Lisp macros require you to avoid variable name capture yourself. So, for a macro which iterates over the lines in a file, this is wrong:

(defmacro with-file-lines ((line file) &body forms)
  ;; wrong
  `(with-open-file (in ,file)
     (do ((,line (read-line in nil in)
                 (read-line in nil in)))
         ((eq ,line in))

It’s wrong because it binds in to the stream open to the file, and user code could perfectly legitimately refer to a variable of the same name.

The standard approach to dealing with this is to use gensyms:

(defmacro with-file-lines ((line file) &body forms)
  ;; righter
  (let ((inn (gensym)))
    `(with-open-file (,inn ,file)
     (do ((,line (read-line ,inn nil ,inn)
                 (read-line ,inn nil ,inn)))
         ((eq ,line inn))

This creates a new symbol bound to inn (in’s name), and then uses it as the name of the variable bound to the stream. Code can’t then use any variable with this unique name.

This works, but it’s ugly. Metatronic macros let you write the above like this:

(defmacro/m with-file-lines ((line file) &body forms)
  ;; righter, easier
  `(with-open-file (<in> ,file)
     (do ((,line (read-line <in> nil <in>)
                 (read-line <in> nil <in>)))
         ((eq ,line <in>))

In this macro all symbols which look like <> (in any package) are rewritten to unique names, but all references to symbols with the same original name are to the same symbol1. This makes this common case more pleasant to do: macros written using defmacro/m have less noise around their expansion.

Metatronic macros go to some lengths to avoid leaking the rewritten symbols. Given this silly macro

(defmacro/m silly ()

then (eq (silly) (silly)) is false. Similarly given this:

(defmacro/m also-silly (f)
  `(eq ,f '<silly>))

Then (also-silly '<silly>) will be false of course.

There is defmacro/m, macrolet/m and define-compiler-macro/m, and the implementation of metatronization is exposed if you need it.

Documentation is here, source code is here. It will be available in Quicklisp in due course.

  1. in fact, a symbol whose name is <> is rewritten as a unique gensym as a special case. I am not sure if this is a good thing but it’s what happens.