"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
.
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>
- Starts a series of rules: the empty first argument denotes any identifiers.
- This is the pattern to match against,
- ... and this is the resulting code.
We can see how it works:
gosh$ (when-true (= 0 0) (display "hi") (display "bye") (newline)) hibye #<undef> gosh$ (when-true (= 0 1) (display "hi") (display "bye") (newline)) #<undef>
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)) a b c #<undef>
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 ...)) ...) (cond ((eq? method (quote method-id)) (apply method-id args)) ... (else (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)))) make-enemy gosh$ (define hobgob (make-enemy "Hobgoblin" 25)) hobgob gosh$ (hobgob 'get-name) "Hobgoblin" gosh$ (hobgob 'dead?) #f gosh$ (hobgob 'damage-me "club" 10) You attack Hobgoblin, doing 10 damage!
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))))))
-
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