Share
Embed
Macros All the Way Down
Presented at TriClojure meetup on 2018-07-26.
This talk gives a brief introduction to Scheme macros to people familiar with Clojure. Although some of the code is Racket-oriented, they are easily adaptable to work with many other Scheme implementations.
Chris Jester-Young
July 26, 2018
Transcript
23 slides
-
For Scheme purists My talk will cover some Racket-specific features.
Some of these features have been ported to other Schemes. If your Scheme doesn’t have them, I’m sorry. I grew up in the world of syntax objects (Guile and Racket mainly). If your Scheme doesn’t use syntax objects, I’m sorry. -
Cheatsheet for Clojurians Racket Clojure (let ([x 1] [y 2])
(+ x y)) (let [[x y] [1 2]] (+ x y)) (let* ([x 1] [y 2]) (+ x y)) (let [x 1 y 2] (+ x y)) '(foo bar baz) '(foo bar baz) `(foo ,(+ 42 10)) `(~'foo ~(+ 42 10)) `(foo ,@(map sqrt '(1 2 3))) `(~'foo ~@(map #(Math/sqrt %) '(1 2 3))) (foo #:bar 42 #:baz 10) (foo :bar 42 :baz 10) (lambda (foo #:bar [bar 42]) (+ foo bar)) (fn [foo & {bar :bar :or {bar 42}}] (+ foo bar)) Go to https://goo.gl/jkjSAN to keep this on hand. -
More tips for Clojurians Commas are not whitespace: ,x reads
as (unquote x). ,@x reads as (unquote-splicing x). `x reads as (quasiquote x). In Scheme, quasiquote is a macro, not a reader macro. -
Racket-specific goodies In Racket, square and curly brackets are treated
the same as round brackets by default. (foo bar), [foo bar], and {foo bar} are treated the same. Vector literals are written as #(a b c), and hash literals are written as #hash((k1 . v1) (k2 . v2) (k3 . v3)). Vector and hash literals are quoted by default. Use quasiquote if you want dynamic values. -
And here we are (and (odd? x) (even? y)) 㱺
(if (odd? x) (even? y) #f) Macro code (define-syntax-rule (and expr1 expr2) (if expr1 expr2 #f)) In Scheme, true is written as #t and false is written as #f. #f is the only falsy value, and every other value is truthy. (Later on we will cover how to write a variadic version of this macro.) -
Or so I hear (or (even? x) (odd? y)) 㱺
(let ((x (even? x))) (if x x (odd? y))) Macro code (define-syntax-rule (or expr1 expr2) (let ((x expr1)) (if x x expr2))) Scheme macros are hygienic. Here, the x in the expansion is treated as a different identifier from the x in expr1. There is no need to explicitly use gensyms. -
Let there be light (let ([a 20] [b 42] [c
100]) (+ a b c)) 㱺 ((lambda (a b c) (+ a b c)) 20 42 100) (define-syntax-rule (let ([id value] ...) body0 body* ...) ((lambda (id ...) body0 body* ...) value ...)) Macro code -
Sweet syntactic sugar We’ve seen define-syntax-rule so far. It is
actually a macro that uses define-syntax and syntax-rules behind the scenes: (define-syntax-rule (and expr1 expr2) (if expr1 expr2 #f)) 㱺 (define-syntax and (syntax-rules () [(and expr1 expr2) (if expr1 expr2 #f)])) For simple macros, define-syntax-rule usually suffices. For something more complex, we need to use syntax-rules directly. -
Again and again Armed with syntax-rules, we can use recursion
to implement a variadic version of the and macro: (define-syntax and (syntax-rules () [(and) #t] [(and expr) expr] [(and expr rest ...) (if expr (and rest ...) #f)])) -
And again and again This defines a recursive and macro
with 3 cases: No expressions: and is defined to return true. One expression: simply evaluate the expression. Multiple expressions: if the first expression is truthy, then recurse on the remainder of the expressions, otherwise return false. -
Proof of the pudding (and (odd? x) (even? y) (positive?
z)) 㱺 (if (odd? x) (if (even? y) (positive? z) #f) #f) Note that the single-expression case is required to avoid having (and (positive? z)) expand into (if (positive? z) #t #f). You can implement a variadic or macro using a similar technique. But there is another way to implement or, using cond, which we’ll look at later. -
Speaking of cond As you probably know, cond is a
macro implemented on top of if. Let’s try our hand at writing a simple version! For now, let’s assume all cond cases are of either the (test expr) form or the (else expr) form. -
Here’s my version (define-syntax cond (syntax-rules (else) [(cond) (void)] [(cond
(else expr)) expr] [(cond (test expr) rest ...) (if test expr (cond rest ...))])) This version is very simple and leaves a lot to be desired in terms of user-friendly error handling. But it’s a reasonable starting point. -
Procedural macros So far, we’ve been looking at declarative macros
only. These work for simple cases, but more complex macros require real code. Classic Lisp macros work with datums. Scheme macros work with syntax objects. Attaches additional context to allow macro hygiene to work transparently. -
And here we go again Syntax transformers are simply functions
that receive a syntax object and return another syntax object. Here, we’ll implement and using a procedural macro: (define-syntax and (lambda (stx) (syntax-case stx () [(_) #'#t] [(_ expr) #'expr] [(_ expr rest ...) #'(if expr (and rest ...) #f)]))) -
#'? What on earth? Racket (and other Scheme implementations that
use syntax objects) has 4 extra reader macros: #'foo reads as (syntax foo). #`foo reads as (quasisyntax foo). #,foo reads as (unsyntax foo). #,@foo reads as (unsyntax-splicing foo). Obviously, these are syntax object analogues for quote, quasiquote, unquote, and unquote-splicing. -
Or else? (define-syntax or (lambda (stx) (define (build-cond-clauses exprs) (syntax-case
exprs () [(expr) #'((else expr))] [(expr rest ...) #`((expr) #,@(build-cond-clauses #'(rest ...)))])) (syntax-case stx () [(or) #'#f] [(or expr ...) #`(cond #,@(build-cond-clauses #'(expr ...)))]))) -
Questions? This barely scratches the surface of writing macros, but
should get you started. I could easily get to Level −20 and beyond if I were to keep writing these slides! I’m always happy to answer any questions you have about writing macros, both now and later. You can email me at [email protected], though I'm easier to reach on Discord at cky#6493.