--- title: "Macros and Quasiquote" output: arl::arl_html_vignette pkgdown: as_is: true vignette: > %\VignetteIndexEntry{Macros and Quasiquote} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") arl::register_knitr_engine() ``` Macros let you transform code at compile time, before evaluation. Arl provides `defmacro` for defining macros, quasiquote/unquote for building output templates, automatic hygiene to prevent accidental variable capture, `gensym` for generating unique symbols, and `capture` for intentionally breaking hygiene (anaphoric macros). ## Overview: what kind of macros does Arl support? Arl macros are **procedural, defmacro-style** macros: each macro is an ordinary function that receives its arguments as unevaluated syntax and returns new syntax. This is the same model used by Common Lisp and Clojure, as opposed to Scheme's pattern-based `syntax-rules`. On top of this, Arl adds **automatic hygiene**. Bindings introduced by a macro body are automatically renamed so they cannot shadow the caller's variables. This means that simple macros are hygienic *by default* -- you do not need to call `gensym` for every temporary. When you do want to intentionally introduce a binding visible to the caller (an anaphoric macro), you use `capture` to opt out of hygiene for specific symbols. In summary: - **Hygienic by default** -- macro-introduced `define`, `let`, and `lambda` bindings are automatically renamed. - **Unhygienic escape via `capture`** -- marks a symbol so hygiene leaves it alone, allowing intentional capture. - **`gensym` available** -- generates unique symbols for cases where you need explicit control (e.g. macro-time computation that builds binding forms dynamically). ## Defining a macro ```{arl} ;;' @description Evaluate body forms when test is truthy. (defmacro when (test . body) `(if ,test (begin ,@body) #nil)) ``` With that macro defined: ```{arl} ;; expands to: (if (> 5 3) (begin (print "yes")) #nil) ;; note that the first `[1] "yes"` is the return value, ;; while the second `"yes"` is the print output (when (> 5 3) (print "yes")) ``` ```{arl, include=FALSE} (assert-equal "yes" (when (> 5 3) "yes")) ``` Macros receive their arguments *unevaluated* -- the forms are passed as syntax trees (R calls/symbols), not as values. The macro body constructs and returns new syntax, which the compiler then compiles and evaluates in place of the original call. You can document macros with `;;'` annotation comments above the definition (see [Getting Started -- Documenting functions](getting-started.html#documenting-functions)), and use `help` to view this documentation from the REPL: ```{arl, eval=FALSE} (help "when") ``` ## Macro parameters ### Fixed parameters ```{arl} (defmacro double (x) `(* 2 ,x)) (double (+ 1 2)) ; => 6 ``` ```{arl, include=FALSE} (assert-equal 6 (double (+ 1 2))) ``` ### Rest parameters Use `.` to collect remaining arguments into a list: ```{arl} (defmacro unless (test . body) `(if ,test #nil (begin ,@body))) (unless (= 1 2) (+ 20 22)) ; => 42 ``` ```{arl, include=FALSE} (assert-equal 42 (unless (= 1 2) 42)) ``` ### Optional parameters with defaults Parameters wrapped in a pair `(name default)` are optional: ```{arl} (defmacro greet ((name "world")) `(string-concat "hello, " ,name)) (greet) ; => "hello, world" (greet "Alice") ; => "hello, Alice" ``` ```{arl, include=FALSE} (assert-equal "hello, world" (greet)) (assert-equal "hello, Alice" (greet "Alice")) ``` You can mix required and optional parameters, and combine them with rest: ```{arl} (defmacro opt-rest ((x 1) . rest) `(list ,x ,@rest)) (opt-rest) ; => (1) (opt-rest 2 3 4) ; => (2 3 4) ``` ```{arl, include=FALSE} (assert-equal (list 1) (opt-rest)) (assert-equal (list 2 3 4) (opt-rest 2 3 4)) ``` ### Pattern destructuring in parameters Use `(pattern ...)` to destructure a macro argument (i.e., unpack it into multiple variables, as in Python's `x, y = obj`): ```{arl} (defmacro let-pair ((pattern (name value))) `(define ,name ,value)) (let-pair (x 10)) x ; => 10 ``` ```{arl, include=FALSE} (assert-equal 10 x) ``` Patterns can be nested, have defaults, or be used as rest parameters: ```{arl} ;; Nested pattern (defmacro deep ((pattern (a (b c)))) `(list ,a ,b ,c)) (deep (1 (2 3))) ; => (1 2 3) ;; Pattern with default (defmacro with-point ((pattern (x y) (list 0 0))) `(+ ,x ,y)) (with-point) ; => 0 (with-point (3 4)) ; => 7 ``` ```{arl, include=FALSE} (assert-equal (list 1 2 3) (deep (1 (2 3)))) (assert-equal 0 (with-point)) (assert-equal 7 (with-point (3 4))) ``` ## Quasiquote, unquote, and splicing Quasiquote (backtick) is the primary tool for constructing macro output. Inside a quasiquoted template, `,expr` (unquote) evaluates `expr` and inserts the result into the template, and `,@expr` (unquote-splicing) evaluates `expr` (which must produce a list) and splices its elements into the template on the same level as `,@expr`. ### Quasiquote basics ```{arl} (define x 10) `(+ ,x 20) ; => (+ 10 20) ``` ```{arl, include=FALSE} (assert-equal (list '+ 10 20) `(+ ,x 20)) ``` Concisely, the difference between `,` and `,@` is that `,` always replaces `,expr` with one element, but `,@`, if the list to be spliced has length greater than 1, can replace `,@expr` with more than one element. ### Splicing lists ```{arl} (define xs (list 2 3)) `(+ 1 ,@xs 4) ; => (+ 1 2 3 4) ``` ```{arl, include=FALSE} (assert-equal (list '+ 1 2 3 4) `(+ 1 ,@xs 4)) ``` ### Nested quasiquote When quasiquotes are nested, each level of backtick adds one to the depth, and each comma subtracts one. Only depth-zero unquotes are evaluated: ```{arl} `(a `(b ,,x)) ; => (a (quasiquote (b (unquote 10)))) ``` ```{arl, include=FALSE} (assert-equal '(a (quasiquote (b (unquote 10)))) `(a `(b ,,x))) ``` This is useful for writing macros that generate other macros. ## Hygiene Arl macros are **hygienic by default**. When a macro introduces bindings (via `define` or other macros that expand into a `define`), those bindings are automatically renamed so they cannot collide with names at the call site. ### Automatic hygiene example Consider a `swap` macro that uses a temporary variable: ```{arl} (defmacro swap (a b) `(let ((temp ,a)) (set! ,a ,b) (set! ,b temp))) ``` Without hygiene, if the caller had a variable named `temp`, the macro would silently shadow it. In Arl, the macro-introduced `temp` is automatically renamed to a fresh symbol, so caller bindings are safe: ```{arl} (define temp 999) (define x 1) (define y 2) (swap x y) x ; => 2 y ; => 1 temp ; => 999 (unaffected) ``` ```{arl, include=FALSE} (assert-equal 2 x) (assert-equal 1 y) (assert-equal 999 temp) ``` ### How it works After a macro function returns its expansion, the expander runs a *hygienization* pass. This pass walks the expanded code and renames any symbols that the macro introduced (i.e. not originating from the call site) in binding positions like those of `define` or `lambda` parameters. Symbols originating from the caller's code are marked and left untouched. The quasiquote expander tags each evaluated value with its origin, marking unquoted caller expressions in particular as having originated from the containing scope. The hygienizer then uses these tags to decide which symbols to rename. ## `gensym` -- generating fresh symbols `gensym` creates a unique symbol guaranteed not to collide with any user-defined name. Use it when you need to create bindings at macro-expansion time that must not conflict: ```{arl} (gensym) ; => G__N for some N (gensym "tmp") ; => tmp__N for some N ``` ```{arl, include=FALSE} (assert-true (symbol? (gensym))) (assert-true (symbol? (gensym "tmp"))) ``` Each call returns a new symbol with a monotonically increasing counter, incrementing further to skip over any symbols which are already defined. ### When to use `gensym` Because Arl macros are hygienic by default, you usually do *not* need `gensym` for simple temporaries in quasiquoted output. `gensym` is useful when you build forms *programmatically at expansion time* and need a consistent temporary name across all of them -- for example, when `map` generates a variable number of forms that must share a binding: ```{arl} ;; bind-all: evaluate expr once, then define each name to the result. (defmacro bind-all (expr . names) (define tmp (gensym "val")) `(begin (define ,tmp ,expr) ,@(map (lambda (n) `(define ,n ,tmp)) names))) (bind-all (+ 1 2) a b c) (list a b c) ``` ```{arl, include=FALSE} (assert-equal 3 a) (assert-equal 3 b) (assert-equal 3 c) ``` Here `map` builds one `define` per name, and every generated form must reference the same temporary. Because these forms are constructed programmatically rather than written directly in a quasiquote template, automatic hygiene won't save us: we need `gensym` to ensure the binding is unique. Another common pattern is loop macros: ```{arl} (defmacro do-list (binding . body) (begin (define var (car binding)) (define seq (car (cdr binding))) (define remaining (gensym "do_list_remaining")) `(begin (define ,remaining (_as-list ,seq)) (while (not (null? ,remaining)) (define ,var (car ,remaining)) ,@body (set! ,remaining (cdr ,remaining)))))) (do-list (x '(1 2 3)) (print x)) ``` ## `capture` -- intentional variable capture (anaphoric macros) Sometimes you *want* a macro to introduce a binding visible to the caller. The classic example is the **anaphoric if** (`aif`), which binds the test result to the fixed symbol `it` so the then/else branches can use it. Without any special handling, hygiene would rename the macro's `it` to a fresh symbol, making it invisible to the caller. The `capture` builtin overrides this: it marks a specific symbol as "intentionally introduced", telling the hygienizer to leave it alone. ### Signature ```{arl, eval=FALSE} (capture 'symbol expr) ``` - `symbol` -- the symbol name to preserve (quoted). - `expr` -- the expression in which occurrences of `symbol` should remain unhygienic. ### Example: anaphoric if ```{arl} (defmacro aif (test then alt) `(let ((it ,test)) (if it ,(capture 'it then) ,(capture 'it alt)))) ``` Usage: ```{arl} (import display :refer (string-concat)) (aif (+ 2 3) (string-concat "result is " it) ; it => 5 "no result") ; => "result is 5" ``` ```{arl, include=FALSE} (assert-equal "result is 5" (aif (+ 2 3) (string-concat "result is " it) "no result")) ``` Here `capture` is applied to the `then` and `alt` expressions (which came from the caller). It marks every occurrence of `it` inside those expressions as "introduced" rather than "call_site", so hygiene does not rename them. Meanwhile, the `(let ((it ,test)) ...)` binding itself is in the macro's quasiquoted output, and `capture` ensures the two sides agree on the name `it`. ### How `capture` interacts with hygiene 1. The macro returns a quasiquoted expansion containing `let ((it ...)) ...`. 2. Normally, hygiene would rename the macro's `it` to a fresh symbol. 3. `capture` walks `then` and `alt` and marks each `it` symbol with origin `"introduced"`. 4. Because both the binding and the references now have the same origin, the hygienizer treats them as belonging to the same scope and does not rename them. Without `capture`, the caller would not be able to refer to `it`: ```{arl, eval=FALSE} ;; BROKEN -- without capture, hygiene renames 'it' (defmacro bad-aif (test then alt) `(let ((it ,test)) (if it ,then ,alt))) (bad-aif (+ 2 3) it 0) ; it here refers to the CALLER's 'it', not the macro's ``` ```{arl, include=FALSE} ;; Verify the broken aif: caller's symbol is unbound, so it should error. ;; Use a fresh name (not 'it') to avoid picking up the binding leaked by ;; the working aif example above under devtools::load_all(). (defmacro bad-aif (test then alt) `(let ((it ,test)) (if it ,then ,alt))) (assert-error (bad-aif (+ 2 3) unbound-caller-sym 0)) ``` ## Compile-time computation Because the macro body is ordinary Arl code executed at expansion time, you can perform arbitrary computation before returning the template: ```{arl} (defmacro const-multiply (a b) (let ((result (* a b))) `(quote ,result))) (const-multiply 6 7) ; => 42 (computed at macro-expansion time) ``` ```{arl, include=FALSE} (assert-equal 42 (const-multiply 6 7)) ``` ## Recursive and composing macros Macros can call themselves recursively: ```{arl} ;; Thread-first: recursive macro that threads a value through forms (defmacro -> (value . forms) (if (null? forms) value (let ((first-form (car forms)) (rest-forms (cdr forms))) (if (list-or-pair? first-form) `(-> (,(car first-form) ,value ,@(cdr first-form)) ,@rest-forms) `(-> (,first-form ,value) ,@rest-forms))))) ;; expands to (- (* (+ 5 3) 2) 1) (-> 5 (+ 3) (* 2) (- 1)) ; => 15 ``` ```{arl, include=FALSE} (assert-equal 15 (-> 5 (+ 3) (* 2) (- 1))) ``` Macros can also expand into calls to other macros. The expander recursively expands until no macro calls remain: ```{arl} ;; when-not expands into unless, which expands into if (defmacro when-not (test . body) `(unless ,test ,@body)) ;; two levels of expansion: when-not -> unless -> if (when-not (= 1 2) (+ 20 22)) ; => 42 ``` ```{arl, include=FALSE} (assert-equal 42 (when-not (= 1 2) (+ 20 22))) ``` ## Inspecting macro expansions ### From Arl Use the `macroexpand` function to perform macro expansion on an expression at execution time. `macroexpand` supports an optional `depth` argument: - no depth: fully and recursively expands all macro calls - `depth = 1` (or `:depth 1`): one expansion step at the outermost call - `depth = N`: exactly `N` expansion steps `macroexpand-1` is a convenience alias for one-step expansion, equivalent to `(macroexpand expr 1)` and `(macroexpand expr :depth 1)`: ```{arl} (macroexpand '(when #t 1) 1) ; => (if #t (begin 1) #nil) (macroexpand '(when #t 1) :depth 1) ; => (if #t (begin 1) #nil) (macroexpand-1 '(when #t 1)) ; => (if #t (begin 1) #nil) ``` ```{arl, include=FALSE} (assert-equal '(if #t (begin 1) #nil) (macroexpand '(when #t 1) 1)) (assert-equal '(if #t (begin 1) #nil) (macroexpand '(when #t 1) :depth 1)) (assert-equal '(if #t (begin 1) #nil) (macroexpand-1 '(when #t 1))) ``` For full recursive expansion, use `macroexpand` with no depth (or its alias `macroexpand-all`): ```{arl} (macroexpand '(when #t (unless #f 42))) ``` ```{arl, include=FALSE} (assert-true (list? (macroexpand '(when #t (unless #f 42))))) ``` `macro?` tests whether a symbol names a currently defined macro: ```{arl} (macro? 'when) ; => #t (macro? 'car) ; => #f ``` ```{arl, include=FALSE} (assert-true (macro? 'when)) (assert-false (macro? 'car)) ``` ### From R Use `engine$macroexpand()`, which takes the same `depth` argument, to expand from R: ```{r eval=FALSE} engine <- Engine$new() engine$eval(engine$read("(defmacro when (test . body) `(if ,test (begin ,@body) #nil))")[[1]]) engine$macroexpand(engine$read("(when #t 1)")[[1]]) ``` To see the full compilation pipeline (parsed, expanded, compiled R code), use `engine$inspect_compilation(text)`. It returns a list with `parsed`, `expanded`, `compiled`, and `compiled_deparsed`. See `?Engine` for details. ## Real-world examples from the standard library As in most Lisps, many of Arl's core forms are implemented as macros. Here are some patterns worth studying: ### `let` -- parallel bindings with `gensym` ```{arl, eval=FALSE} (defmacro let (bindings . body) (if (null? bindings) `(begin ,@body) (begin (define temps (map (lambda (b) (gensym "tmp")) bindings)) (define patterns (map car bindings)) (define values (map (lambda (b) (car (cdr b))) bindings)) `((lambda ,temps ,@(map (lambda (pair) `(define ,(car pair) ,(car (cdr pair)))) (zip patterns temps)) (begin ,@body)) ,@values)))) ``` ```{arl, include=FALSE} ;; Verify let-like macro works by defining a differently-named copy (defmacro my-let (bindings . body) (if (null? bindings) `(begin ,@body) (begin (define temps (map (lambda (b) (gensym "tmp")) bindings)) (define patterns (map car bindings)) (define values (map (lambda (b) (car (cdr b))) bindings)) `((lambda ,temps ,@(map (lambda (pair) `(define ,(car pair) ,(car (cdr pair)))) (zip patterns temps)) (begin ,@body)) ,@values)))) (assert-equal 3 (my-let ((x 1) (y 2)) (+ x y))) ``` This macro generates one `gensym` per binding, then creates a `lambda` with those fresh names as parameters. Inside the lambda body, each fresh name is destructured into the user's pattern. The `gensym` calls are necessary here because the binding names are computed programmatically rather than appearing literally in a quasiquoted template. ### `loop`/`recur` -- Clojure-style iteration The `loop` macro rewrites `recur` calls into a `while` loop with flag variables, all using `gensym` to avoid conflicts: ```{arl} (import looping :refer (loop recur)) (loop ((n 5) (acc 1)) (if (= n 0) acc (recur (- n 1) (* acc n)))) ; => 120 ``` ```{arl, include=FALSE} (assert-equal 120 (loop ((n 5) (acc 1)) (if (= n 0) acc (recur (- n 1) (* acc n))))) ``` ### `try`/`catch`/`finally` -- syntax sugar The `try` macro parses its clause list at expansion time and rewrites into calls to the lower-level `try` function (which in turn calls R's `tryCatch`): ```{arl} (try-catch (stop "something went wrong") (catch e (string-concat "caught: " ($ e "message"))) (finally (display "cleanup"))) ``` ```{arl, include=FALSE} (assert-true (string? (try-catch (stop "test error") (catch e (string-concat "caught: " ($ e "message")))))) ``` ## Related guides - [Getting Started](getting-started.html) - [Language Reference](lang-reference.html) - [Standard Library: Core, R Interop, and Testing](lang-core.html) - [Examples](examples.html#macro-examples)