Harry R. Schwartz

Code writer, sometime Internet enthusiast, attractive nuisance.

The author at the Palais du Luxembourg in Paris, November 2022. hacker news gitlab sourcehut pinboard librarything 1B41 8F2C 23DE DD9C 807E A74F 841B 3DAE 25AE 721B

Vancouver

British Columbia

Canada

vegan


The Littlest Macro

hrs

Published .
Tags: lisp, computer-science, emacs.

A PDF of this article is available.

Functions are extraordinarily useful, but there are certain things they can’t do. For example, when a function is evaluated in a language with eager evaluation (that is, most popular languages), every argument to the function is recursively evaluated before the function runs. Generally this is what we’d want, but in certain cases we want to delay the evaluation of the function’s arguments until later. This isn’t possible with a regular function.

For example, some languages (Ruby, for instance) feature an unless operator. This operator works a lot like if. It takes a condition, a consequent, and an alternative, just like an if, but it negates the condition before picking a branch.

Suppose we want to implement an unless operator in Emacs Lisp. Our first inclination might be to implement this as a function:

(defun unless (condition consequent alternative)
  (if (not condition) consequent alternative))

This seems reasonable, right? Let’s try running it with C-x C-e:

(unless t
  (print "evaluating the consequent!")
  (print "evaluating the alternative!"))
"evaluating the consequent!"
"evaluating the alternative!"
"evaluating the alternative!"

Oops! We evaluated both the consequent and the alternative.

The second alternative is the return value of the whole unless expression; it’s harmless.

That’s definitely not what we wanted, and it happened because the arguments to the function were evaluated before the conditional was called.

Let’s rewrite this as a macro:

(defmacro unless (condition consequent alternative)
  `(if (not ,condition)
       ,consequent
       ,alternative))

We can use macroexpand to display the code that our macro generates:

(macroexpand '(unless t (+ 1 2) (- 1 2)))

Which yields:

(if (not t) (+ 1 2) (- 1 2))

Now that we’ve reimplemented unless as a macro, let’s try running it again:

(unless t
  (print "evaluating the consequent!")
  (print "evaluating the alternative!"))
"evaluating the alternative!"
"evaluating the alternative!"

Again, that second alternative is the return value of the whole unless expression. But, woo! We’ve effectively added a whole new construct to Emacs Lisp. Users can now use unless in the same way that they could use if.

Because they let you add new constructs to the language, macros are extremely powerful. In fact, most of the features of Emacs lisp are implemented as macros. Just macro-expand them and see for yourself!

(macroexpand '(when t foo bar))
(if t (progn foo bar))

Macros allow you to arbitrarily manipulate a program’s abstract syntax tree. This manipulation is usually centered around the idea of delaying a chunk of computation. This delay is done by treating a statement as a data structure, which is easy because Lisp is homoiconic. Some non-Lisps are homoiconic, too: Julia and Elixir are among them.

This isn’t something that you’ll often need in your everyday programming, but once it’s part of your arsenal it becomes pretty useful. By manipulating the syntax tree directly you’re effectively extending the language, and that’s an awfully powerful idea.


You might like these textually similar articles: