2022-07-15: Scheme: Macros - Introduction

"More recently, Scheme became the first programming language to support hygienic macros, which permit the syntax of a block-structured language to be extended in a consistent and reliable manner. ... Scheme programs can define and use new derived expression types, called macros." (R7RS Report)

Macros are an important element of Scheme, introduced as an optional extension in the Appendix of R4RS, but included in the main report body of R5RS onwards.

In the Scheme Primer, we see some introductory examples, but with non-RnRS definitions. Here are those examples rewritten using syntax-rules.

One-Sided If

R7RS already includes when, but we can define an equivalent ourselves (called when-true to avoid name clash):

(define-syntax when-true
  (syntax-rules ()                        ; <1>
    ((when-true test stmt1 ...)           ; <2>
     (if test (begin stmt1 ...)))))       ; <3>
  1. Starts a series of rules: the empty first argument denotes any identifiers.
  2. This is the pattern to match against,
  3. ... and this is the resulting code.

We can see how it works:

gosh$ (when-true (= 0 0) (display "hi") (display "bye") (newline))
gosh$ (when-true (= 0 1) (display "hi") (display "bye") (newline))

Iterate Over List

The next example is a for loop, to iterate over list, simplifying the use of for-each:

(define-syntax for 
  (syntax-rules () 
    ((for (item lst) body ...) 
     (for-each (lambda (item) body ...) lst))))

gosh$ (for (str '("a" "b" "c")) (display str) (newline))

Methods on a Value

The following example adds what can be thought of as methods to a value:

(define-syntax methods
  (syntax-rules ()
    ((methods ((method-id method-args ...)
                              body ...) ...)
     (lambda (method . args)
      (letrec ((method-id
                (lambda (method-args ...)
                 body ...)) ...)
        ((eq? method (quote method-id))
         (apply method-id args)) ...
         (error "No such method:" method))))))))

And their example works:

gosh$ (define (make-enemy name hp)
......        (methods
......         ((get-name)
......          name)
......         ((damage-me weapon hp-lost)
......          (cond
......           ((dead?)
......            (format #t "Poor ~a is already dead!\n" name))
......           (else
......            (set! hp (- hp hp-lost))
......            (format #t "You attack ~a, doing ~a damage!\n"
......                    name hp-lost))))
......         ((dead?)
......          (<= hp 0))))
gosh$ (define hobgob (make-enemy "Hobgoblin" 25))
gosh$ (hobgob 'get-name)
gosh$ (hobgob 'dead?)
gosh$ (hobgob 'damage-me "club" 10)
You attack Hobgoblin, doing 10 damage!

Using Identifiers

The second argument to syntax-rules consists of zero or more identifiers. In the examples above, the list of identifiers is always empty. A good example of using identifiers in syntax-rules is found in the definition of cond and case in section 7.3 of the R7RS report; both else and => are identifiers.

We can take advantage of this by writing a simple repeat ... until loop. Observe how identifiers are matched literally in the pattern, like until, and not used as a placeholder for an expression, like stmt and test.

(define-syntax repeat
  (syntax-rules (until)             ; <1>
    ((repeat stmt ... until test)
     (let loop ()
       (begin stmt ...)
       (unless test (loop))))))
  1. until is listed as an identifier.

And an example of using this:

gosh$ (let ((i 0))
......  (repeat
......    (display "i is ") (display i) (newline)
......    (set! i (+ 1 i))
......    until (= i 10)))
i is 0
i is 1
i is 2
i is 3
i is 4
i is 5
i is 6
i is 7
i is 8
i is 9

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-29.