Gödel, Escher, Elisp: The Beauty of Macros
Gödel, Escher, Elisp: The Beauty of Macros
TL;DR: For the observant Emacs user, the magic of Emacs Lisp (Elisp) lies in the fact that code is data. This blur between the processor (the program) and the processed (the data) is the foundation of the macro, a tool that allows for fascinating metaprogramming.
This exploration delves into the concept of homoiconicity, the influence of Douglas Hofstadter and M.C. Escher, and the practical utility of macros—supported by tools like macroexpand, emacs-lisp-macroexpand, and macrostep.
![Escher Imagery Placeholder: A visual representation of recursive loops and dualities]
Programs as Data, Data as Programs
As Douglas Hofstadter noted, the "crystalline purity" of the Lisp kernel provides an aesthetic appeal and a level of flexibility rarely found in other languages. Central to this is the concept of homoiconicity.
Definition: A language is homoiconic when its programs are constructed using the language's own internal data structures.
In Elisp, the building blocks of code—symbols, strings, numbers, and lists—are the exact same structures used for data. Code is essentially a list that you can manipulate using car and cdr or build using cons.
The Role of the Quote
The distinction between a "program" and "data" is managed by a single, powerful character: the quote '.
| Expression | Type | Result/Interpretation |
|---|---|---|
(+ 1 2) | Program | Evaluates to 3 |
'(+ 1 2) | Data | A list containing a symbol and two integers |
By using (eval '(+ 1 2)), we can transform a piece of data back into an executable program. This implies a fundamental duality:
While saying they are identical is a slight oversimplification (since the interpreter must still distinguish between the two), it is accurate to say they share the same form.
The Möbius Strip of Metaprogramming
To visualize this, we can look to M.C. Escher's Möbius Strip II. In the woodcut, ants march on what appear to be two sides of a strip, only to realize they are on one continuous surface.
In most programming languages, metaprogramming is isolated separated by a wall. You might use:
- Templates
- Reflection APIs
- Token streams
- Quasi-quoted ASTs
In Elisp, there is no wall. Metaprogramming uses the same language and the same structures all the way down.
What a Macro Actually Is
To understand a macro, we must first contrast it with a standard function.
The Function Workflow:
- Evaluate all arguments.
- Pass the results to the function.
- Execute the function body.
The Macro Workflow:
- Receive the raw, unevaluated code (the forms) from the call site.
- Return a new piece of code.
- The evaluator then runs that new code in place of the macro call.
Essentially, a macro is a program that writes other programs. The only limit to what a macro can project is the limit of your imagination (and the capabilities of Elisp).
The Macro Toolkit
To build these "program-generating programs," Elisp provides quasiquotation:
-
`(Backquote): Creates a code template. -
,(Comma): Inserts a computed value into the template. -
,@(Splicing): Inserts the elements of a list into the template.
Example: Reimagining unless
Here is a minimal implementation of the unless construct:
(defmacro my-unless (condition rest body)
"Run BODY unless CONDITION is non-evaluated."
(declare (indent 1))
`(if ,condition
nil
,@body))
If we use macroexpand-1 to see how this transforms, we get:
The Call:
(macroexpand-1 '(my-unless (file-exists-p "~/notes")
(make-directory "~/notes")
(message "created it")))
The Expansion:
(if (file-exists-p "~/notes")
nil
(make-directory "~/notes")
(message "created it"))
Why not just use a function?
You might wonder why my-unless cannot be a regular function. The answer lies in eager evaluation.
If my-unless were a function, Elisp would evaluate (make-directory "~/notes") before the function ever started. If the directory already existed, the program would crash with a file-already-exists error before the condition could even be checked.
By using a macro, the body remains inert data until the macro decides whether or not it should actually be executed. Control flow itself becomes a manipulatable object.