Macros

Macros are the centerpiece of any Lisp. Macros provide a mechanism for modifying the code of your program using other program code. A macro declaration looks like a function declaration but is declared using defmacro instead of defn.

(defmacro macro-name (args ...)
  ...)

Macros can only be declared at module scope, not inside of a class. When a macro is called, its arguments are not evaluated. Instead, it is given the syntax of the arguments directly and should return the syntax of a new GDLisp expression to use in its place.

This is best demonstrated with some examples. when is a macro built into GDLisp that evaluates a conditional and then runs the code inside if (and only if) the conditional is true. Essentially, when is an if block that does not have an “else” branch and for which the “true” case can be longer than one expression.

If GDLisp did not provide this macro, we could write it as follows:

(defmacro when (condition &rest body)
  `(if ,condition
     (progn ,.body)))

Let’s break this down, as it uses several new features. When the macro is called, it must be called with at least one argument. The first argument, as an abstract syntax tree, will be assigned to condition, and the rest will be accumulated into a list and passed as body. Inside the macro, we use a new form of syntax called quasiquoting.

Any GDLisp expression can be quoted. By calling the quote special form, or equivalently putting a single quotation mark ' before it, evaluation of the expression is delayed. For literals like strings or numbers, this has no effect, as strings and numbers are self-evaluating forms. For lists and symbols, quoting the value returns a reified representation of the value. That is, (if c t f) is an if expression that will evaluate c and then branch, but '(if c t f) evaluates to a literal list of four elements, all of which are symbols in this example.

Quoting is useful when we have a constant expression that we wish to reify at runtime. But often, especially with macros, we wish to have a mostly constant expression with some unknown values interpolated in. For this, we use quasiquoting. You can think of quasiquoting as being sort of like string interpolation in Python or Ruby, but for arbitrary expressions, not just strings.

A quasiquote begins with the backtick `, which expands to the (quasiquote ...) special form. Inside the quasiquote, everything is interpreted literally with the following two exceptions.

  • (unquote ...) blocks (equivalently, a prefix comma ,) will be evaluated and spliced as an element into the result.

  • (unquote-spliced ...) blocks (equivalently, a prefix ,.) will be evaluated and concatenated into the result. The enclosing context must be either a list or an array.

To see the difference, consider the following:

;; Assume the variable x has the value (2 3 4)
`(1 x 5)   ; No unquote, evaluates to (1 x 5)
`(1 ,x 5)  ; Regular unquote, evaluates to (1 (2 3 4) 5)
`(1 ,.x 5) ; Spliced unquote, evaluates to (1 2 3 4 5)

Looking back at our when example:

(defmacro when (condition &rest body)
  `(if ,condition
     (progn ,.body)))

The when macro, when called, returns an S-expression whose head is if. The first argument to if is our condition. Then the second argument is a progn which runs the entire body (as a list of expressions) if the condition is true.

We could write its opposite, unless, in a similar way:

(defmacro unless (condition &rest body)
  `(if (not ,condition)
     (progn ,.body)))

Macros also work in declaration context, both at module scope and inside of a class. We’ve already seen one such macro: defvars.

(defmacro defvars (&rest args)
  (let ((var-decls (list/map (lambda (name) `(defvar ,name)) args)))
    `(progn ,.var-decls)))

Note that the let block is not part of the expanded code. The let block is code that’s run when the macro is called and is used to compute the macro expansion, which is a progn consisting of several defvar special forms in a row.

Note

You may be wondering how progn works here, since it’s an expression that evaluates expressions in order, while defvar is clearly a declaration that only makes sense at class or module scope.

The answer is that progn is actually deep magic. It’s even more special than other “special” forms, in that progn is the one thing in GDLisp that is valid fully-expanded in both expression or declaration context (or both, incidentally, but that can only occur in the command line REPL).

GDLisp provides several macros built-in, which you’ll grow accustomed to using as a matter of course (as mentioned before, if itself is a macro, written in terms of cond). For end-user games, you might never write defmacro at all, but it’s an invaluable tool for library authors who wish to do advanced code generation. And even in a runnable game, you might find yourself automating the boring bits of code generation from time to time as well.