5 Macros
To a first approximation, macros in Racket are like macros in Curly, except that
instead of let-macro to bind a local macro, define-syntax defines a macro for a module body.
instead of an S-expression, a Racket macro receives a syntax object as its argument, and it must produce a syntax object as its results; and
instead of having an unchangeable Curly language that is used to implement macros, the syntactic forms and primitives used to implement a Racket macro are determined by an explicit require with for-syntax.
5.1 Defining Macros
The following module defines and uses the macro five, which simply expands to 5:
#lang racket/base (require (for-syntax racket/base)) (define-syntax five (lambda (stx) #'5)) (+ (five) (five anything-here x [y z] ((1 2)) "3"))
Running this program produces 10, because it simply adds 5 and 5. The transformer function for five takes a stx argument, but ignores it, so that’s why (five) and (five anything-here x [y z] ((1 2)) "3"). If we want a five that works only when uses as (five), then the transformer just needs to inspect stx:
#lang racket/base (require (for-syntax racket/base)) (define-syntax five (lambda (stx) (if (= 1 (length (syntax->list stx))) #'5 (raise-syntax-error #f "bad syntax" stx)))) (+ (five) (five anything-here x [y z] ((1 2)) "3"))
Passing stx as the third argument to raise-syntax-error allows DrRacket to highlight the erroneous expression in pink, since the syntax object carries its source location to be recorded as part of the exception.
Instead of writing an explicit lambda, macros are often written using a function shorthand built into define-syntax that is like the one built into define:
#lang racket/base (require (for-syntax racket/base)) (define-syntax (five stx) (if (= 1 (length (syntax->list stx))) #'5 (raise-syntax-error #f "bad syntax" stx))) (+ (five) (five))
The five macro is not useful, but we can use just these tools to build time-it:
#lang racket/base (require (for-syntax racket/base)) (define (time-thunk thunk) (let* ([before (current-inexact-milliseconds)] [answer (thunk)] [after (current-inexact-milliseconds)]) (printf "It took ~a ms to compute.\n" (- after before)) answer)) (define-syntax (time-it stx) (datum->syntax #'here (list #'time-thunk (list #'lambda #'() (list-ref (syntax->list stx) 1))))) (time-it (+ 1 2))
More compactly, using quasisyntax:
(define-syntax (time-it stx) #`(time-thunk (lambda () #,(list-ref (syntax->list stx) 1))))
With a check to make sure that time-it is used correctly:
(define-syntax (time-it stx) (unless (= 2 (length (syntax->list stx))) (raise-syntax-error #f "bad syntax" stx)) #`(time-thunk (lambda () #,(list-ref (syntax->list stx) 1))))
5.2 Run Time and Compile Time
As another example, suppose that we want a lightweight way to run external programs. Instead of using library functions like system* and find-executable-path in
(system* (find-executable-path "ls") "-l")
we’d like to write just
(run ls -l)
To make this run form work, the identifiers ls and -l have to be treated as literal symbols and converted into strings, instead of treating them as variable references.
#lang racket/base (require racket/system (for-syntax racket/base)) (define (run-program prog-str . arg-strs) (apply system* (find-program prog-str) arg-strs)) (define (find-program str) (or (find-executable-path str) (error 'pfsh "could not find program: ~a" str))) (define-syntax (run stx) #`(run-program #,@(map (lambda (id) #`(symbol->string '#,id)) (list-tail (syntax->list stx) 1)))) (run ls -l)
With this implementation of run, the expression
(run ls -l)
expands to
(run-program (symbol->string 'run) (symbol->string 'ls) (symbol->string '-l))
But we’d prefer to perform the conversion once and for all runs at compile time, so that the expansion would be
(run-program "run" "ls" "-l")
To do that, we need to make sure the symbol->string call is in a compile-time position instead of within generated run-time code:
(define-syntax (run stx) #`(run-program #,@(map (lambda (id) (symbol->string (syntax-e id))) (list-tail (syntax->list stx) 1))))
5.3 Exercises
Implement the macro define-five, which is like define, but the behavior of define-five is specified only when the right-hand expression has the value five:
(define-five x (+ 4 1)) (+ x x) ; => 10 Your macro should implement the obvious optimization, but it should also warn the programmer about the optimization’s assumption at compile time. For example, the output of
(define-five x (+ 4 1)) (+ x x) (define-five y (+ 3 3)) (+ y y) should be
Assuming expression produces 5: (+ 4 1) Assuming expression produces 5: (+ 3 3) 10 10 Note that both printouts occur first, since they happen at compile time, and the first 10 prints only when run time start.
You can print an S-expression se using
(printf "~s\n" se)
Change define-five to print its warning at run time instead of compile time. So, the output of
(define-five x (+ 4 1)) (+ x x) (define-five y (+ 3 3)) (+ y y) should be
Assuming expression produces 5: (+ 4 1) 10 Assuming expression produces 5: (+ 3 3) 10