Skip to content

Instantly share code, notes, and snippets.

@sebastiancarlos
Last active March 8, 2025 16:11
Show Gist options
  • Save sebastiancarlos/c25b0fb656c1ee67df260e3c24642c70 to your computer and use it in GitHub Desktop.
Save sebastiancarlos/c25b0fb656c1ee67df260e3c24642c70 to your computer and use it in GitHub Desktop.
Explanation of the ONCE-ONLY Common Lisp macro.
;; Deus Vult, Compilamus
;; The following code is compliant with the Holy See's ANSI Standard Of 2054.
:; Here's an example of a classic Lisp macro, ONCE-ONLY, which ensures
;; parameters to a Lisp macro are executed only once, along with a dense
;; explanation.
;; Example implementation of ONCE-ONLY
(defmacro do-primes ((var start end) &body body)
(once-only (start end)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for n in names collect (gensym))))
`(let (,@(loop for g in gensyms collect `(,g (gensym))))
`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
,@body)))))
;; Explanation of ONCE-ONLY:
;- This classic macro is used to generate code that evaluates certain macro
; arguments once only and in a particular order.
; - It can be a bit tricky to follow, so keep in mind the following:
; - When you see a double-backquote (``), we are generating code with a
; single backquote.
; - Even if the backquotes are not back to back, it still usually means
; that you are generating code which, at some point, contains an internal
; backquote.
; - Observing multiple commas (,,) is also a sign of this (more on
; multiple commas later)
; - Sometimes, the ",@" form is used even if evaluating into a containing
; list
; - For example "(,@(list 1 2))" is the same as ",(list 1 2)", as both
; evaluate to "(1 2)"
; - However, the ",@" syntax might be preferred as it's more explicit
; about the fact that we are inserting multiple forms on a given space.
; - Nested backquotes (and multiple commas):
; - We can think of it like this:
; - only one backquote gets collapsed per evaluation (the outermost one)
; - If there are multiple commas, the leftmost one belongs to the
; leftmost backquote.
; - Substitutions are made only for unquoted components appearing at the
; same nesting level as the outermost backquote
; - Keep the following in mind regarding "macros" and "macro-writing macros"
; - A macro contains:
; - 1. (Optional) Extra code which runs at "macro expansion time", which
; helps produce 2.
; - 2. Result of the macro. It's the code which should run at "runtime"
; when calling the macro. Usually it's a backquote expression, so that
; the macro parameters can be evaluated into it.
; - A macro-writing macro contains:
; - 1. (Optional) Extra code which runs at "macro-writing-macro expansion
; time", which helps produce 2.
; - 2. Result of the macro-writing macro. Usually it's a backquote
; expression, so that the macro-writing macro parameters can be
; evaluated into the code. 2 itself consists of both 3 and 4.
; - 3. (Optional) Extra code which runs at "macro expansion time", which
; helps produce 4.
; - 4. Result of the macro. Code which runs at "runtime". Usually it's a
; backquote expression, so that the macro parameters AND/OR
; macro-writing macro parameters can be evaluated into it.
; - An explanation of ONCE-ONLY would be:
; - Note that, on usage, you should pass a body to ONCE-ONLY, which is a
; backquoted form containing unqoutes of the uses of the variables passed
; - First, a list of gensyms is created for every name passed. This is done
; at "macro-writing macro expansion time"
; - Indeed, two rounds of generating gensyms are necessary because
; once-only is a macro itself, and so it has to be hygienic for its own
; sake. And so, this first "gensyms" is used for "once-only" itself.
; - A backquote is evaluated, containing code to run at "macro expansion
; time"
; - This code instantiates the original gensyms to new gensyms at
; "macro-expansion time". The new gensyms will be used on the macro.
; - Then, it returns a final backquote, which will be what runs at runtime.
; - It instantiates the macro gensyms to the parameters to the macro
; - At macro expansion time. it creates a final instantiation of the names
; to the values of the names, so that the runtime body can use them as
; expected, God willing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment