Reconstructing Clojure Macros With Speclj
“It is a revelation to compare Menard’s Don Quixote with Cervantes’. The latter, for example, wrote (part one, chapter nine): ‘…truth, whose mother is history, rival of time, depository of deeds, witness of the past, exemplar and adviser to the present, and the future’s counselor.’ Written in the seventeenth century, written by the ‘lay genius’ Cervantes, this enumeration is a mere rhetorical praise of history. Menard, on the other hand, writes: ‘…truth, whose mother is history, rival of time, depository of deeds, witness of the past, exemplar and adviser to the present, and the future’s counselor.’”
Macros are hard,
but one of the most helpful exercises I’ve found in my limited macro
writing experience is practicing by recreating some of the core
macros I already know and love, like
Using speclj greatly simplifies this exercise, and writing macro specs often test my understanding better than writing the implementations themselves. Here’s an example spec for the threading macro:
1 2 3 4 5 6 7 8 9 10 11 12 13
come in very handy.
Macroexpand-1 returns the “first” expansion of a
macro form (macros within macros won’t be expanded).
macroexpand-1 until the expansion is no longer a macro form.
In the above example, the first expansion of
->-macro, my threading
macro replacement, returns a form that starts with another call to
->-macro. (I was surprised to find out that this is how the
threading macro works under the hood).
Macroexpand expands into a
until the first item is
*, which is not a macro.
When it’s macros all the way down,
clojure.walk/macroexpand-all) recursively expands all macros in a
given form, resulting in the much simpler expression
(* (+ 1 2) 3)
in the example above. These functions are all hugely helpful for
writing macros and their associated tests.
Here’s my recreation of
->, which passed the spec:
1 2 3 4 5
And bears a pretty strong resemblance to the original source:
1 2 3 4 5 6 7 8 9 10 11
Some macros are pretty easy to reconstruct (but check their documentation to make sure you really understand how they handle different arguments). If you’re well and truly stuck, it’s always possible to check out the original code for inspiration.
The specs and solutions I’ve written so far (mostly low-hanging fruit) are all available on my Github, if you’d like to have a hand at reconstructing Clojure yourself.