Beautiful Racket
Beautiful Racket is a book about making programming languages with Racket.
This library provides the #lang br teaching language used in the book, as well as supporting modules that can be used in other programs.
This library is designed to smooth over some of the small idiosyncrasies and inconsistencies in Racket, so that those new to Racket are more likely to say “ah, that makes sense” rather than “huh? what?”
1 Installation
If you want all the code & documentation, install the package beautiful-racket.
If you just want the code modules (for instance, for use as a dependency in another project) install the package beautiful-racket-lib.
If you just want the br/macro and br/syntax modules, install the package beautiful-racket-macro.
2 Conditionals
(require br/cond) | package: beautiful-racket-lib |
syntax
(while cond body ...)
> (let ([x 42]) (while (positive? x) (set! x (- x 1))) x) 0
> (let ([x 42]) (while (negative? x) (unleash-zombie-army)) x) 42
syntax
(until cond body ...)
> (let ([x 42]) (until (zero? x) (set! x (- x 1))) x) 0
> (let ([x 42]) (until (= 42 x) (destroy-galaxy)) x) 42
3 Datums
(require br/datum) | package: beautiful-racket-lib |
A datum is a literal representation of a single unit of Racket code, also known as an S-expression. Unlike a string, a datum preserves the internal structure of the S-expression. Meaning, if the S-expression is a single value, or list-shaped, or tree-shaped, so is its corresponding datum.
Datums are made with quote or its equivalent notation, the ' prefix (see Quoting: quote and ’).
When I use “datum” in its specific Racket sense, I use “datums” as its plural rather than “data” because that term has an existing, more generic meaning.
procedure
(format-datum datum-form val ...) → (or/c datum? void?)
datum-form : datum? val : any/c?
Two special cases. First, a string that describes a list of datums is parenthesized so the result is a single datum. Second, an empty string returns void (not #f, because that’s a legitimate datum).
> (format-datum '42) format-datums: contract violation
expected: datum?
given: 42
> (format-datum '~a "foo") 'foo
> (format-datum '(~a ~a) "foo" 42) '(foo 42)
> (format-datum '~a "foo bar zam") '(foo bar zam)
> (void? (format-datum '~a "")) #t
> (format-datum '~a #f) #f
procedure
(format-datums datum-form vals ...)
→ (listof (or/c list? symbol?)) datum-form : (or/c list? symbol?) vals : (listof any/c?)
> (format-datums '~a '("foo" "bar" "zam")) '(foo bar zam)
> (format-datums '(~a 42) '("foo" "bar" "zam")) '((foo 42) (bar 42) (zam 42))
> (format-datums '(~a ~a) '("foo" "bar" "zam") '(42 43 44)) '((foo 42) (bar 43) (zam 44))
> (format-datums '42 '("foo" "bar" "zam")) format-datums: contract violation
expected: datum?
given: 42
> (format-datums '(~a ~a) '("foo" "bar" "zam") '(42)) map: all lists must have same size
first list length: 3
other list length: 1
procedure: #<procedure:...lib/br/datum.rkt:30:13>
4 Debugging
(require br/debug) | package: beautiful-racket-lib |
For instance, suppose you wanted to see how first-condition? was being evaluted in this expression:
(if (and (first-condition? x) (second-condition? x)) (one-thing) (other-thing))
You can wrap it in report and find out:
(if (and (report (first-condition? x)) (second-condition? x)) (one-thing) (other-thing))
This code will run the same way as before. But when it reaches first-condition?, you willl see in current-error-port:
(first-condition? x) = #t
You can also add standalone calls to report as a debugging aid at points where the return value will be irrelevant, for instance:
(report x x-before-function) (if (and (report (first-condition? x)) (second-condition? x)) (one-thing) (other-thing))
x-before-function = 42
(first-condition? x) = #t
But be careful — in the example below, the result of the if expression will be skipped in favor of the last expression, which will be the value of x:
(if (and (report (first-condition? x)) (second-condition? x)) (one-thing) (other-thing)) (report x)
syntax
(report* expr ...)
syntax
(report-datum stx-expr)
(report-datum stx-expr maybe-name)
5 Define
(require br/define) | package: beautiful-racket-lib |
This module also exports the bindings from br/macro.
syntax
(define-cases id [pat body ...+] ...+)
> (define-cases f [(f arg1) (* arg1 arg1)] [(f arg1 arg2) (* arg1 arg2)] [(f arg1 arg2 arg3 arg4) (* arg1 arg2 arg3 arg4)]) > (f 4) 16
> (f 6 7) 42
> (f 1 2 3 4) 24
> (f "three" "arguments" "will-trigger-an-error") f: arity mismatch;
the expected number of arguments does not match the given
number
given: 3
arguments...:
"three"
"arguments"
"will-trigger-an-error"
> (define-cases f2 [(f2) "got zero args"] [(f2 . args) (format "got ~a args" (length args))]) > (f2) "got zero args"
> (f2 6 7) "got 2 args"
> (f2 1 2 3 4) "got 4 args"
> (f2 "three" "arguments" "will-not-trigger-an-error-this-time") "got 3 args"
6 Macro
(require br/macro) | |
packages: beautiful-racket-lib, beautiful-racket-macro |
syntax
(define-macro (id pat-arg ...) result-expr ...+)
(define-macro id #'other-id) (define-macro id (lambda (arg-id) result-expr ...+)) (define-macro id transformer-id) (define-macro id syntax-object)
(define-macro (id pat-arg ...) result-expr ...+) If the first argument is a syntax pattern starting with id, then create a syntax transformer for this pattern using result-expr ... as the return value. As usual, result-expr ... needs to return a syntax object or you’ll get an error.
The syntax-pattern notation is the same as syntax-case, with one key difference. If a pat-arg has a name written in CAPS, it’s treated as a named wildcard (meaning, it will match any expression in that position, and can be subsequently referred to by that name). Otherwise, pat-arg is treated as a literal (meaning, it will only match the same expression). If pat-arg is a literal identifier, it will only match another identifier with the same name and the same binding (in other words, identifiers are tested with free-identifier=?).
For instance, the sandwich macro below requires three arguments, and the third must be please, but the other two are wildcards:
Examples:
> (define-macro (sandwich TOPPING FILLING please) #'(format "I love ~a with ~a." 'FILLING 'TOPPING)) > (sandwich brie ham) sandwich: no matching case for calling pattern
in: (sandwich brie ham)
> (sandwich brie ham now) sandwich: no matching case for calling pattern
in: (sandwich brie ham now)
> (sandwich brie ham please) "I love ham with brie."
> (sandwich banana bacon please) "I love bacon with banana."
The ellipsis ... can be used with a wildcard to match a list of arguments. Please note: though a wildcard standing alone must match one argument, once you add an ellipsis, it’s allowed to match zero:
Examples:
> (define-macro (pizza TOPPING ...) #'(string-join (cons "Waiter!" (list (format "More ~a!" 'TOPPING) ...)) " ")) > (pizza mushroom) "Waiter! More mushroom!"
> (pizza mushroom pepperoni) "Waiter! More mushroom! More pepperoni!"
> (pizza) "Waiter!"
The capitalization requirement for a wildcard pat-arg makes it easy to mix literals and wildcards in one pattern. But it also makes it easy to mistype a pattern and not get the wildcard you were expecting. Below, bad-squarer doesn’t work because any-number is meant to be a wildcard. But it’s not in caps, so it’s considered a literal, and it triggers an error:
Examples:
> (define-macro (bad-squarer any-number) #'(* any-number any-number)) > (bad-squarer 0+10i) bad-squarer: no matching case for calling pattern
in: (bad-squarer 0+10i)
The error is cleared when the argument is in caps, thus making it a wildcard:
Examples:
> (define-macro (good-squarer ANY-NUMBER) #'(* ANY-NUMBER ANY-NUMBER)) > (good-squarer 0+10i) -100
You can use the special variable caller-stx — available only within the body of define-macro — to access the original input argument to the macro.
Examples:
> (define-macro (inspect ARG ...) (with-pattern ([CALLER-STX (syntax->datum caller-stx)]) #`(displayln (let ([calling-pattern 'CALLER-STX]) (format "Called as ~a with ~a args" calling-pattern (length (cdr calling-pattern))))))) > (inspect) Called as (inspect) with 0 args
> (inspect "foo" "bar") Called as (inspect foo bar) with 2 args
> (inspect #t #f #f #t) Called as (inspect #t #f #f #t) with 4 args
This subform of define-macro is useful for macros that have one calling pattern. To make a macro with multiple calling patterns, see define-macro-cases. }
(define-macro id #'other-id) If the first argument is an identifier id and the second a syntaxed identifier that looks like #'other-id, create a rename transformer, which is a fancy term for “macro that replaces id with other-id.” (This subform is equivalent to make-rename-transformer.)
Why do we need rename transformers? Because an ordinary macro operates on its whole calling expression (which it receives as input) like (macro-name this-arg that-arg . and-so-on). By contrast, a rename transformer operates only on the identifier itself (regardless of where that identifier appears in the code). It’s like making one identifier into an alias for another identifier.
Below, notice how the rename transformer, operating in the macro realm, approximates the behavior of a run-time assignment.
Examples:
> (define foo 'foo-value) > (define bar foo) > bar 'foo-value
> (define-macro zam-macro #'foo) > zam-macro 'foo-value
> (define add +) > (add 20 22) 42
> (define-macro sum-macro #'+) > (sum-macro 20 22) 42
(define-macro id (lambda (arg-id) result-expr ...+)) If the first argument is an id and the second a single-argument function, create a macro called id that uses the function as a syntax transformer. This function must return a syntax object, otherwise you’ll trigger an error. Beyond that, the function can do whatever you like. (This subform is equivalent to define-syntax.)
Examples:
> (define-macro nice-sum (lambda (stx) #'(+ 2 2))) > nice-sum 4
> (define-macro not-nice (lambda (stx) '(+ 2 2))) > not-nice not-nice: received value from syntax expander was not syntax
received: '(+ 2 2)
(define-macro id transformer-id) Similar to the previous subform, but transformer-id holds an existing transformer function. Note that transformer-id needs to be visible during compile time (aka phase 1), so use define-for-syntax or equivalent.
Examples:
> (define-for-syntax summer-compile-time (lambda (stx) #'(+ 2 2))) > (define-macro nice-summer summer-compile-time) > nice-summer 4
> (define summer-run-time (lambda (stx) #'(+ 2 2))) > (define-macro not-nice-summer summer-run-time) summer-run-time: undefined;
cannot reference an identifier before its definition
in module: top-level
(define-macro id syntax-object)
syntax-object : syntax? If the first argument is an id and the second a syntax-object, create a syntax transformer that returns syntax-object. This is just alternate notation for the previous subform, wrapping syntax-object inside a function body. The effect is to create a macro from id that always returns syntax-object, regardless of how it’s invoked. Not especially useful within programs. Mostly handy for making quick macros at the REPL.
Examples:
> (define-macro bad-listener #'"what?") > bad-listener "what?"
> (bad-listener) "what?"
> (bad-listener "hello") "what?"
> (bad-listener 1 2 3 4) "what?"
syntax
(define-macro-cases id [pattern result-expr ...+] ...+)
As with define-macro, wildcards in each syntax pattern must be in CAPS. Everything else is treated as a literal match, except for the ellipsis ... and the wildcard _.
> (define-macro-cases yogurt [(yogurt) #'(displayln (format "No toppings? Really?"))] [(yogurt TOPPING) #'(displayln (format "Sure, you can have ~a." 'TOPPING))] [(yogurt TOPPING ANOTHER-TOPPING ... please) #'(displayln (format "Since you asked nicely, you can have ~a toppings." (length '(TOPPING ANOTHER-TOPPING ...))))] [(yogurt TOPPING ANOTHER-TOPPING ...) #'(displayln (format "Whoa! Rude people only get one topping."))]) > (yogurt) No toppings? Really?
> (yogurt granola) Sure, you can have granola.
> (yogurt coconut almonds hot-fudge brownie-bites please) Since you asked nicely, you can have 4 toppings.
> (yogurt coconut almonds) Whoa! Rude people only get one topping.
value
syntax
(define-unhygienic-macro (id pat-arg ...) result-expr ...+)
7 Syntax
(require br/syntax) | |
packages: beautiful-racket-lib, beautiful-racket-macro |
syntax
(with-pattern ([pattern stx-expr] ...) body ...+)
> (define-macro (m ARG) (with-pattern ([(1ST 2ND 3RD) #'ARG] [(LEFT RIGHT) #'2ND]) #'LEFT)) > (m ((1 2) (3 4) (5 6))) 3
syntax
(pattern-case stx ([pattern result-expr ...+] ...))
> (define (pc stx) (pattern-case stx [(1ST 2ND 3RD) #'2ND] [(LEFT RIGHT) #'LEFT])) > (pc #'(a b c)) #<syntax:eval:78:0 b>
> (pc #'(x y)) #<syntax:eval:79:0 x>
> (pc #'(f)) pattern-case: unable to match pattern for '(f)
syntax
(pattern-case-filter stxs ([pattern result-expr ...+] ...))
> (pattern-case-filter #'((a b c) (x y) (f)) [(1ST 2ND 3RD) #'2ND] [(LEFT RIGHT) #'LEFT]) '(#<syntax:eval:81:0 b> #<syntax:eval:81:0 x>)
procedure
(prefix-id prefix ... id-or-ids [ #:source loc-stx #:context ctxt-stx]) → (or/c identifier? (listof identifier?)) prefix : (or string? symbol?) id-or-ids : (or/c identifier? (listof identifier?)) loc-stx : syntax? = #f ctxt-stx : syntax? = #f
The optional loc-stx argument supplies the source location for the resulting identifier (or identifiers).
The optional ctxt-stx argument supplies the lexical context for the resulting identifier (or identifiers).
> (define-macro ($-define ID VAL) (with-pattern ([PREFIXED-ID (prefix-id '$ #'ID)]) #'(define PREFIXED-ID VAL))) > ($-define foo 42) > $foo 42
procedure
(suffix-id id-or-ids suffix ... [ #:source loc-stx #:context ctxt-stx]) → (or/c identifier? (listof identifier?)) id-or-ids : (or/c identifier? (listof identifier?)) suffix : (or string? symbol?) loc-stx : syntax? = #f ctxt-stx : syntax? = #f
The optional loc-stx argument supplies the source location for the resulting identifier (or identifiers).
The optional ctxt-stx argument supplies the lexical context for the resulting identifier (or identifiers).
> (define-macro (define-% ID VAL) (with-pattern ([ID-SUFFIXED (suffix-id #'ID '%)]) #'(define ID-SUFFIXED VAL))) > (define-% foo 42) > foo% 42
procedure
(infix-id prefix id-or-ids suffix ... [ #:source loc-stx #:context ctxt-stx]) → (or/c identifier? (listof identifier?)) prefix : (or string? symbol?) id-or-ids : (or/c identifier? (listof identifier?)) suffix : (or string? symbol?) loc-stx : syntax? = #f ctxt-stx : syntax? = #f
The optional loc-stx argument supplies the source location for the resulting identifier (or identifiers).
The optional ctxt-stx argument supplies the lexical context for the resulting identifier (or identifiers).
> (define-macro ($-define-% ID VAL) (with-pattern ([ID-INFIXED (infix-id '$ #'ID '%)]) #'(define ID-INFIXED VAL))) > ($-define-% foo 42) > $foo% 42
procedure
(strip-bindings stx) → syntax?
stx : syntax?
procedure
(replace-bindings stx-source stx-target) → syntax?
stx-source : (or/c syntax? #f) stx-target : syntax?
procedure
(stx-flatten stx) → (listof syntax?)
stx : syntax?
> (define my-stx #'(let ([x 42] [y 25]) (define (f z) (* x y z)) (displayln (f 11)))) > (map syntax->datum (stx-flatten my-stx)) '(let x 42 y 25 define f z * x y z displayln f 11)
8 Indentation
(require br/indent) | package: beautiful-racket-lib |
Helper functions for DrRacket language indenters.
procedure
textbox : (is-a?/c text%) position : (or/c exact-nonnegative-integer? #f)
procedure
(line textbox position) → exact-nonnegative-integer?
textbox : (is-a?/c text%) position : (or/c exact-nonnegative-integer? #f)
procedure
(previous-line textbox position)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) position : (or/c exact-nonnegative-integer? #f)
procedure
(next-line textbox position)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) position : (or/c exact-nonnegative-integer? #f)
procedure
(line-chars textbox line-idx) → (or/c (listof char?) #f)
textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-start textbox line-idx)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-end textbox line-idx)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-start-visible textbox line-idx)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-end-visible textbox line-idx)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-first-visible-char textbox line-idx) → (or/c char? #f)
textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-last-visible-char textbox line-idx) → (or/c char? #f)
textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(line-indent textbox line-idx)
→ (or/c exact-nonnegative-integer? #f) textbox : (is-a?/c text%) line-idx : (or/c exact-nonnegative-integer? #f)
procedure
(apply-indenter indenter-proc textbox-or-str) → string? indenter-proc : procedure? textbox-or-str : (or/c (is-a?/c text%) string?)
procedure
(string-indents str) → (listof exact-nonnegative-integer?)
str : string?
9 Lists
(require br/list) | package: beautiful-racket-lib |
syntax
(values->list values)
> (split-at '(a b c d e f) 3)
'(a b c)
'(d e f)
> (values->list (split-at '(a b c d e f) 3)) '((a b c) (d e f))
syntax
(push! list-id val)
syntax
(pop! list-id)
10 Reader utilities
(require br/reader-utils) | package: beautiful-racket-lib |
procedure
(apply-reader read-syntax-proc source-str) → datum?
read-syntax-proc : procedure? source-str : string?
11 The br teaching languages
#lang br | package: beautiful-racket-lib |
#lang br/quicklang |