━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ COMPARISON OF DIFFERENT MACRO SYSTEMS IN SCHEME ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ So you're trying out Scheme and want to do some metaprogramming magic. You heard a lot about the power of macros in Lisp and want to see what the big deal is. Guides to macros are abundant but there seems to be a lot of different ways to define a macro, and you don't quite understand how they work and what their differences are. Let's see what your options are. Textual substitution? ═════════════════════ This is what most people think of when the word macro comes up. Simply replace all instances of a token with another. It's easily the worst possible type of metaprogramming. It comes with the sole advantage of being trivial to implement. Your preprocessor might as well be an `awk' script. We don't do this. It's extremely easy to shoot yourself in the foot with such a system, especially with Lisp's fragile S-expressions. Although on the other hand, it can give birth to fascinating works of art, like BourneGol [1]. Moving on… [1] https://utcc.utoronto.ca/~cks/space/blog/programming/BourneGol Unhygienic macros ═════════════════ Instead of replacing text, you're replacing AST. After all, we're dealing with S-expressions here. Simple enough! Beware that using unhygienic macros is strongly frowned upon by the Scheme community. `define-macro' ────────────── This is what Common Lisp and Clojure mainly use (as `defmacro'). In fact, it used to be the only viable metaprogramming capability of Lisps (besides `fexpr') for a while (as `macro'). Because it's so easy to implement, most Scheme implementations include it in one way or another. It might be the only macro option available if you're using an old or constrained implementation. ┌──── │ (define-macro (name args) │ body) └──── Listing 1: `define-macro' template How does it work? Think of it like copying over a template in the form of a list and filling in the blanks. After all, Lisp code is just a bunch of lists! Literal parts remain quoted, other parts get evaluated with each expansion. ┌──── │ (define-macro (backwards-values . body) │ `(values ,@(reverse body))) │ │ (define-macro (when condition . body) │ `(if ,condition (begin . body) (void))) └──── Listing 2: A couple simple macros. It's dead simple to write a macro for most purposes this way. However things get harder when you aim for things like composability, and you're dependent on `gensym' to avoid namespace clashes when introducing literals. ┌──── │ (define-macro (while condition . body) │ (let ((loop (gensym))) │ `(let ,loop () │ (if ,condition (begin ,@body) (,loop))))) └──── Listing 3: We need `gensym' to avoid leaking the `loop' token. It's easy to drown in the `unquote' soup in complex macros. At that point, you're better off using something else. Which brings us to… Hygienic macros ═══════════════ Hygienic macros! The idea is that instead of substituting S-expressions in your code, you're defining a piece of syntax that becomes a part of the language. Sounds good! `syntax-rules' ────────────── This is the most popular macro system amongst Schemes. It's what people think of when you say "hygienic macros." Despite being strongly associated with Scheme, it wasn't a part of Scheme until R5RS. (It was an optional package in R4RS.) ┌──── │ (define-syntax name │ (syntax-rules (literals) │ ((pattern) template) │ ((pattern) template) │ ...)) └──── Listing 4: `syntax-rules' template `syntax-rules' is (are?) a set of patterns matched against the form you're calling the macro with. Think of pattern-matching in procedure definitions in Erlang and Haskell. You can use the ellipsis (`...') token to indicate repeated arguments. ┌──── │ ;; quote is necessary to prevent args (a list) from being evaluated │ ;; like a funcall. │ (define-syntax reverse-values │ (syntax-rules () │ ((_ . args) (apply values (reverse (quote args)))))) │ │ ;; Alternatively, we can use the ellipsis. │ (define-syntax reverse-values │ (syntax-rules () │ ((_ arg ...) (apply values (reverse (list arg ...)))))) └──── Listing 5: `syntax-rules' has a rather noisy syntax. The advantage is that not only you don't need to worry about hygiene and you can write versatile macros that cover different patterns with different templates with ease. (Technically you can break hygiene in `syntax-rules' if you push it too hard. [2]) ┌──── │ (define-syntax while │ (syntax-rules () │ ((_ condition . body) │ (let loop () │ (when condition │ (begin . body) │ (loop)))))) └──── Listing 6: `loop' here is guaranteed to be out of scope of the caller. TODO: Add multi-clause example and contrast with defmacro Great! Except, for most purposes, you don't need more than one syntax rule, and the syntax to define even a simple hygienic macro scares off beginners. Racket, being more macro-happy than other Schemes, comes with a simple macro `define-syntax-rule' that collapses `define-syntax' into a single-clause `syntax-rules' with an empty literal list. ┌──── │ (define-syntax define-syntax-rule │ (syntax-rules () │ ((_ (name . pattern) body) │ (define-syntax name │ (syntax-rules () │ ((_ . pattern) body)))))) │ │ (define-syntax-rule (when condition . body) │ (if condition (begin . body) (void))) └──── Listing 7: `define-syntax-rule' for your convenience, and an example use. Albeit clean, it's quite verbose and has a significant weak point compared to `define-macro': Your macro simply transforms patterns to syntax and you don't get to manipulate this process. That is to say, it lacks the capability to evaluate expressions at compile-time. Well, what do we do then? [2] http://okmij.org/ftp/Scheme/Dirty-Macros.pdf `syntax-case' ───────────── Fear not! A standard part of Scheme since R6RS, `syntax-case' introduces the concept of first-class syntax objects and allows you to embed expressions into your declarative macros whilst preserving hygiene. The notation is a tad verbose, but it's very similar to `syntax-rules' in many ways. In addition, the capability to introduce expressions within the transformer allows you to break hygiene as well. ┌──── │ (define-syntax name │ (lambda (syntax-object) │ (syntax-case syntax-object (literals) │ ((pattern) optional-fender syntax) │ ((pattern) optional-fender syntax) │ ...) └──── Listing 8: `syntax-case' template `syntax-case' is versatile enough to implement `syntax-rules' and `define-macro', as well as other helper transformers, such as `make-variable-transformer' or `syntax->list' which you can use to make your own bespoke syntax transformer! ┌──── │ (define-syntax (reverse-values stx) │ (datum->syntax stx (cons values (reverse (cdr (syntax->datum stx)))))) └──── Listing 9: A simple macro showcasing `datum->syntax' and `syntax->datum' transformers. ┌──── │ ;;; Hey, why not? │ (define-syntax λ (identifier-syntax lambda)) │ │ (define-syntax (syntax-rules stx) │ (syntax-case stx () │ ((_ (literal ...) ((name . pattern) body) ...) │ (syntax │ (λ (stx) │ (syntax-case stx (literal ...) │ ((_ . pattern) (syntax body)) ...)))))) └──── Listing 10: `syntax-rules' implemented in `syntax-case'. Note that unlike `syntax-rules' (which returns a syntax transformer), `syntax-case' itself is a transformer, and needs to return a `syntax' value. The form allows an optional guard form (called a "fender") for each clause that tests the syntax passed to the transformer to see if it's valid, even if it matches the pattern. You can use it to check for disallowed types and throw a syntax error. TODO: Fender example Where to go from here? ══════════════════════ Aforementioned macro systems should be enough to cover 99% of your needs. You can stop reading here if you just wanted a quick rundown of differences between these three popular macro systems. But certain specific Scheme implementations have their own macro systems and using them can make your code integrate well with the rest of the environment. Let's see what they are… TODO: the rest Racket ────── Syntax parameters ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ Even more helper transformers ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ◊ Using `match' `syntax-parse' ╌╌╌╌╌╌╌╌╌╌╌╌╌╌ And beyond… ╌╌╌╌╌╌╌╌╌╌╌ CHICKEN ─────── Procedural macros ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ Chez ──── `extend-syntax' ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ Conclusion ══════════ When in doubt, use `syntax-rules'.