Now the truth can be told: the br language that we’ve used throughout this book is a not-very-special dialect of Racket. It’s just the foundational Racket language—which is called #lang racket/base—with some extra Racket libraries automatically imported, and some br-specific libraries intended to defer a few complications until later in your Racket career. (Namely: now.)
There’s nothing wrong with using #lang br in modules or projects. But as a developer, it’s a more virtuous habit to use #lang racket/base instead. Why?
As a rule of thumb, it’s better practice to start with the foundational language—that is, racket/base—and use require to import what you need. This makes your project (a little) faster to start up, and reduces dependencies.
Both #lang br and this book will continue to be revised and enlarged. #lang racket/base, by contrast, is guaranteed to remain the foundational configuration of Racket.
racket/base is ubiquitous throughout other Racket projects, so recognizing common racket/base idioms is essential for being able to read other people’s code.
A few options for those switching from br to racket/base:
Rather than using #lang br, you can import just the parts you like. The br language is built from smaller modules like br/list, br/define, and br/syntax. These are all compatible with racket/base. See the documentation for what’s included in each.
The br code is available under the MIT license. Rather than depend on the br package, you’re welcome to copy the code—individual functions, or whole modules—into your own projects, or adapt it.
None of the code is especially complicated, though some of it uses techniques not covered in this book. (If you make improvements, I welcome pull requests at the beautiful-racket repo).
You can also just learn to rewrite your code without the br idioms and patterns. You can peek inside the br source code to see what’s going on, but these are the highlights.
define-macro and define-macro-cases are lightly disguised forms of define-syntax with syntax-parse. Here are three equivalent ways of writing the same macro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | (define-macro (mac1 X bar Y) #'(list X "second" Y)) (define-macro-cases mac2 [(mac2 X bar Y) #'(list X "second" Y)]) (require (for-syntax syntax/parse)) (define-syntax (mac3 caller-stx) (syntax-parse caller-stx [((~literal mac3) X (~literal bar) Y) #'(list X "second" Y)] [else (raise-syntax-error 'mac3 (format "no matching case for calling pattern in: ~a" (syntax->datum caller-stx)))])) (mac1 "first" bar "third") ; '("first" "second" "third") (mac2 "first" bar "third") ; '("first" "second" "third") (mac3 "first" bar "third") ; '("first" "second" "third") |
Under the hood, define-macro is just a shorthand version of the one-case instance of define-macro-cases, which is a shorthand version of the full define-syntax + syntax-parse macro.
define-syntax is Racket’s basic tool for introducing a macro. define-syntax can only be used to bind an identifier (like mac3, in the example above) to a function that takes one syntax object as an argument, and returns another syntax object.
Racket has a number of tools that create syntax transformers, or help process the input of a macro. syntax-parse is in the latter category: it’s similar to cond or case, in that it takes a syntax object as input and tries to match it to a series of clauses with a syntax pattern on the left. When a match is found, the right side of the clause is evaluated. The (bar) argument in syntax case is a list of identifiers that will be matched literally.
Key differences between define-macro or define-macro-cases and define-syntax with syntax-parse:
In define-macro and define-macro-cases, the caller-stx value is available, but hidden by default. In define-syntax, it’s explicit. + In your own define-syntax macros, you don’t have to name the input argument caller-stx—we’re using that name here to show the correspondence between the forms. More often, it’s just called stx.
In define-macro and define-macro-cases, if no else clause is included, one is automatically generated with a friendly error message.
In define-macro and define-macro-cases, pattern variables in the syntax pattern have to be written in UPPERCASE, and everything else is treated as a literal match. In syntax-parse, this presumption is reversed: every identifier in a syntax pattern is treated as a pattern variable, and literals have to be specially designated. + You can still use UPPERCASE for pattern variables, but in syntax-parse it’s an option, not a requirement.
syntax-parse is Racket’s most powerful macro-making tool. This summary only scratches the surface. If you’re looking for a midrange option, consider syntax-case, which works mostly the same way as syntax-parse, but has fewer moving parts.
Similarly, with-pattern is an alternative to Racket’s with-syntax.
1 2 3 4 5 6 7 8 9 | (define stx #'(foo bar 42)) (syntax->datum (with-pattern ([(FIRST SECOND THIRD) stx]) #'(list THIRD SECOND FIRST))) ; '(list 42 bar foo) (syntax->datum (with-syntax ([(FIRST SECOND THIRD) stx]) #'(list THIRD SECOND FIRST))) ; '(list 42 bar foo) |
Key differences:
In with-pattern, pattern variables matched in earlier lines can be used in later lines. In with-syntax, they cannot. + But see with-syntax*, which permits references to previously matched pattern variables.
In with-pattern, as with define-macro and define-macro-cases, pattern variables in the syntax patterns have to be written in UPPERCASE. Everything else is treated as a literal match. In with-syntax, this presumption is reversed: every identifier in a syntax pattern is treated as a pattern variable. There is no way to match an identifier literally.
prefix-id and suffix-id are convenient ways of using format-id (which in turn is just a convenient way of using datum->syntax). The major difference is that prefix-id and suffix-id automatically generate identifiers that live in the same lexical context as the base identifier. Whereas with datum->syntax and format-id, you always have to provide an explicit lexical-context argument:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | (require racket/syntax) (define-macro (make-bars) (define id #'foo) (with-pattern ([BAR1 (datum->syntax id 'bar1)] [BAR2 (format-id id "~a" 'bar2)] [BAR23 (suffix-id #'BAR2 "3")]) #'(begin (define BAR1 'BAR1) (define BAR2 'BAR2) (define BAR23 'BAR23) (list BAR1 BAR2 BAR23)))) (make-bars) ; '(bar1 bar2 bar23) |
define-cases: a variant of case-lambda, modified to be more similar in form to define-macro-cases, but otherwise works the same way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | (define f (case-lambda [() 10] [(x) x] [(x y) (list y x)] [r r])) (list (f) (f 1) (f 1 2) (f 1 2 3)) ; '(10 1 (2 1) (1 2 3)) (define-cases g [(g) 10] [(g x) x] [(g x y) (list y x)] [(g . r) r]) (list (g) (g 1) (g 1 2) (g 1 2 3)) ; '(10 1 (2 1) (1 2 3)) |
There is no until or while in Racket, but they can be easily derived from unless and when (as they are in br/cond):
#lang br/quicklang is a variant of the #lang br language for expander modules that automatically exports four interposition points: #%top, #%app, #%datum, and #%top-interaction. As above, if you’re embarking on your own language, you should build on racket/base, because br/quicklang does nothing that you couldn’t do yourself in two lines of code.