Skip to content
HN On Hacker News ↗

Gödel, Escher, Elisp: The Beauty of Macros

▲ 106 points 39 comments by JNRowe 3w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this text is a mix of AI and human-written content.

36 %

AI likelihood · overall

Mixed
67% human-written 33% AI-generated
SEGMENTS · HUMAN 3 of 12
SEGMENTS · AI 2 of 12
WORD COUNT 1,268
PEAK AI % 88% · §8
Analyzed
Aug 14
backend: pangram/v3.3
Segments scanned
12 windows
avg 106 words each
Distribution
67 / 33%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,268 words · 12 segments analyzed

Human AI-generated
§1 Human · 9%

Gödel, Escher, Elisp: The Beauty of Macros Table of Contents 1. TLDR 2. Programs as Data, Data as Programs   emacs elisp lisp 3. What a Macro Actually Is   emacs elisp macros 4. You've Been Using Macros All Along   emacs elisp macros 5. Rolling Your Own   emacs elisp macros config 6. Strange Loops and Drawing Hands   emacs elisp hofstadter geb 7. Seeing Through the Magic   emacs elisp tooling 8. With Great Power   emacs elisp macros 9. Bending the Metal   emacs elisp lisp 1. TLDR If you are an Emacs user with a keen eye, you will have noticed that in Emacs Lisp, code is data. After all, 'Lisp' is shorthand for 'List Processing'. One of Elisp's most beautiful features is the fortuitous blur between the thing that is processing the list (the program) and the list itself (the data). The macro in Elisp is a utility that exploits this blur and allows you to leverage this dualism between program and data in many useful and fascinating ways. In this post, I want to swoon about macros, explain what "homoiconic" actually means, demonstrate their ubiquity in Elisp, depict their beauty on a detour through Hofstadter's strange loops and Escher's prints, and finally show off some tooling (macroexpand, emacs-lisp-macroexpand, macrostep) that enhances both comprehension and appreciation of macros. Here is the Escher imagery we'll be leaning on along the way: 2. Programs as Data, Data as Programs   emacs elisp lisp The kernel of Lisp has a crystalline purity that not only appeals to the esthetic sense, but also makes Lisp a far more flexible language than most others. — Douglas Hofstadter An important word for this post is homoiconic. A language is homoiconic when its programs are written in the language's own data structures. Few languages are homoiconic, and the Lisp family wears the property most proudly, with Emacs Lisp (Elisp) being the dialect many of us are most familiar with. In Elisp, source code is lists, symbols, strings, and numbers. Code looks exactly the same as lists you build with cons and take apart with car and cdr. The distinction between program and data is exhibited by a specific, special character, the glorious ': ;; a program: evaluates to 3 (+ 1 2) ;; data: a list of three elements — a symbol and two numbers '(+ 1 2) The quote tells the evaluator not to run the form that follows, but to treat it as plain data (a list). To emphasize that program and data are equivalent in Elisp, running eval on the quoted list (as in (eval '(+ 1 2))) will turn it into a program, where the function is addition, and its arguments are the numbers 1 and 2.

§2 Mixed · 68%

That dualism lies at the heart of the language. Any piece of code is one character away from being a value you can inspect, transform, and rebuild; and any suitably-shaped value is one function call away from being a program.

§3 Human · 28%

So in Elisp, we say Program = Data, even though that's a little too simplistic, because we saw how correctly the Lisp interpreter deciphers when a list is being represented as a program versus when it is being represented as data…. The point is the list: that's the unifying form. Maybe more appropriately, we can say the program and data take the same form, or as previously mentioned, Elisp's programs are written in Elisp's own data structures. This post was motivated by a simultaneous obsession with Douglas Hofstadter's writing and Elisp macros, so be prepared for many depictive metaphors from one of Hofstadter's favourite artists, M.C. Escher. Here's the first: Escher drew this kind of dualism as a woodcut. The ants of Möbius Strip II appear to march on both sides of a strip. The image is provocative enough at first glance, but I invite you to follow any one of them around and discover that the two sides are one continuous surface. Program and data are the two sides of Elisp's homoiconic Möbius. Figure 1: M.C. Escher, Möbius Strip II (1963). Two sides, one surface. © The M.C. Escher Company.

§4 AI · 79%

In most languages, metaprogramming lives in a separate layer with its own representation of code like templates, reflection APIs, token streams, quasi-quoted ASTs. Some of those layers are crude and some are genuinely sophisticated, but each is a wall between code and data. In Elisp there was never a wall to tunnel through.

§5 Mixed · 38%

Elisp enables metaprogramming, but it's the same language, and the same data structures, all the way down. 3. What a Macro Actually Is   emacs elisp macros Consider a regular function in Elisp.

§6 Mixed · 65%

A function receives values and computes a value at runtime. On the other hand, a macro receives code (the raw, unevaluated forms typed at its call site) and returns new code, which is then evaluated in its place. Macros run at expansion time, before your program does. I like to think of macros as little programs that write other programs given the arbitrary forms they can accept. The complexity of that form -> program projection is essentially infinite, or at least bounded by what you can express in Elisp, which is very likely bounded by your imagination. The macro's form -> program toolkit is quasiquotation: backquote ` builds a code template, comma , inserts a computed piece, and ,@ splices in a list. Here's the smallest real macro I can write, a reimplementation of unless: (defmacro my-unless (condition &rest body) "Run BODY unless CONDITION is non-nil."

§7 Mixed · 43%

(declare (indent 1)) `(if ,condition nil ,@body)) We can actually ask Emacs what this macro will get expanded to. The first code block is the call to macroexpand-1, the second is the expansion it returns: (macroexpand-1 '(my-unless (file-exists-p "~/notes") (make-directory "~/notes") (message "created it"))) (if (file-exists-p "~/notes") nil (make-directory "~/notes") (message "created it")) To anticipate a common question: why couldn't my-unless be a function?

§8 AI · 88%

Function arguments are evaluated eagerly, before the function ever sees them. A function version would evaluate (make-directory "~/notes") while its arguments were being prepared, before the condition could ever be consulted. So, when ~/notes already exists, instead of doing nothing it would signal a file-already-exists error, and the message would never run at all.

§9 Human · 15%

In other words, the function receives the results of the body, but the point of using a macro here is to decide whether the body runs at all. A macro receives the body as inert data, so control flow itself is up for grabs. In this way, you are extending what the language can express. 4. You've Been Using Macros All Along   emacs elisp macros Macros may seem specialist or eccentric…. I hope this surprises the Elispiens who are reading this! It certainly surprised me! when and unless are macros over if. dolist and dotimes are macros over while.

§10 Mixed · 62%

push, pop, and setf are macros that rewrite themselves into the right mutation for the place you provide them with. with-current-buffer, with-temp-buffer, and ignore-errors are macros that wrap your code in the correct save-and-restore ceremony so you never have to type it.

§11 Mixed · 33%

Even defun is a macro! The most justifiably famous macro in any Emacs config is use-package: (use-package magit :bind ("C-c g" . magit-status) :hook (git-commit-mode .

§12 Mixed · 61%

flyspell-mode)) :bind and :hook aren't Elisp, but rather keywords in a small configuration language, and the use-package macro is its 'compiler', expanding the declaration into the require calls, keymap bindings, hooks, and autoload deferrals that would otherwise need to be written out by hand in their full, verbose form. The define-minor-mode macro is similar in this way. One declaration expands into a variable, an interactive toggle command, keymap wiring, and documentation.