On this page:
5.1 Defining Macros
5.2 Run Time and Compile Time
5.3 Exercises
7.4.0.4

5 Macros

To a first approximation, macros in Racket are like macros in Curly, except that

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

  1. 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)

  2. 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