In this tutorial, we'll discuss the construction of programs at compile time using what are called macros.
Some Racket forms like and
and or
look like function calls, but we
know they are not because they support short-circuit evaluation. We
can define forms that behave differently than function calls (or that
look like function calls, but actually do other things as well, like
the rackunit
check-
forms). These forms are sometimes called
macros.
Redefining and
The simplest way of defining a macro in racket is to use define-syntax-rule. As a first approximation, you can think of these syntax rules as functions that don't evaluate their arguments.
Let's start by defining
And
, which is just like the lower case version except the short-circuit evaluation is from the right.
#lang racket
(define-syntax-rule (And a b)
(if b a #f))
(module+ test
(require rackunit)
(define (die)
(error 'die "don't run this"))
(check-equal? (And (die) #f) #f)
(check-exn exn:fail? (lambda () (and (die) #f))))
- Notice the use of
lambda
to delay evaluation incheck-exn
; that's exactly the kind of boilerplate we can eliminate with a syntax rule:
(module+ test
(define-syntax-rule (check-fail expr)
(check-exn exn:fail? (lambda () expr)))
(check-fail (and (die) #f))
(check-fail (And #f (die))))
- Macros are a key part of the implimentation strategy of racket. What's an example of some syntax we saw already that is (probably) implimented as a macro?
Redefining or
- Let's define
Or
in terms ofif
in a similar way toAnd
. It should pass the following tests
(module+ test
(check-equal? (Or #t #t) #t)
(check-equal? (Or #f #t) #t)
(check-equal? (Or #t #f) #t)
(check-equal? (Or (die) #t) #t)
(check-fail (or (die) #t)))
Let and Let*
In homework 3 we implimented an evalator for with*
using with
.
It turns out that let*
can be implimented in a similar way in terms
of let
using a macro.
Let's start by reviewing let*
:
(module+ test
(check-equal? (let* ([x 5]
[y (- x 3)])
(+ y y))
4)
(check-equal? (let* ([x 5]
[y (- x 3)]
[y x])
(* y y))
25))
- The key insight is that
let*
behaves like a nested set oflet
s. Let us transform the examples about according to this idea:
(module+ test
(check-equal? (let ([x 5])
(let ([y (- x 3)])
(+ y y)))
4)
(check-equal? (let ([x 5])
(let ([y (- x 3)])
(let ([y x])
(* y y))))
25))
That's more cumbersome to write, of course, but that's why
let*
exists. So how can we formulate this recursively? Well, racket syntax already looks like a list, so lets turn this into a recursive problem on lists. Start a new filelet-transformer.rkt
Complete the following skeleton for the
let-transformer
module.
(provide let-transformer)
(define (let-transformer lst)
(match lst
[(list 'Let* '() body) ]
[(list 'Let* (cons (list ) tail) body)
(list 'let (list (list id val))
(let-transformer
(list 'Let* )))]))
(module+ test
(require rackunit)
(check-equal? (let-transformer '(Let* ([x 5]
[y (- x 3)])
(+ y y)))
'(let ([x 5]) (let ([y (- x 3)]) (+ y y)))))
From list transformers to macros.
- Prerequisites
- match
In this last part we'll show how to extend the Racket Compiler in a quite general way.
Start a new file
let-macros.rkt
. Roughly speaking, the following takes the input racket codestx
, turns it into a list, processes with the function we defined already, and then turns it back into racket code.
(require (for-syntax racket/match))
(require (for-syntax "let-transformer.rkt"))
(define-syntax (Let* stx)
(datum->syntax #'here (let-transformer (syntax->datum stx))))
- We can also use
syntax-case
, which is roughly speakingmatch
for macros. This eliminates the need to write a seperate transformer function. It's also harder to debug. (Let^
is just the macro name, so as not to collide withLet*
)
(define-syntax (Let^ stx)
(syntax-case stx ()
[(Let^ () body) #'body]
[(Let^ ((first-id first-val) (id val) ...) body)
#'(let ([first-id first-val])
(Let^ [(id val) ...] body))]))
(module+ test
(require rackunit)
(check-equal? (Let^ ([x 5] [y (- x 3)]) (+ y y)) 4)
(check-equal? (Let^ ([x 5] [y (- x 3)] [y x]) (* y y)) 25))