You are looking at historical revision 32939 of this page. It may differ significantly from its current revision.

Procedural macros made easy

The Scheme standard, R5RS, only provides declarative macros based on syntax-rules. They are easy to use, but rather limited. For example, you can only create hygienic macros, you have no control over the expansion process, in particular, you can't use local procedures to be evaluated at compile time. To overcome this limitations, R6RS offers syntax-case macros, but that's a mouthfull ...

Fortunately, Chicken offers two versions of procedural macros, explicit and implicit renaming macros. They offer full flexibility without any limitations but are tedious to use.

First, you must care to avoid variable capture with renaming, if you want hygienic macros, or you must decide which variables should be captured on purpose. Implicit renaming here helps a lot: You simply inject names which you want to be captured, the others are renamed automatically by the runtime system.

Second, you must do the destructuring of the macro code by hand. Wouldn't it be nice, if this could be done automatically behind the scene as well?

This library provides the means for this to happen. Combining implicit renaming with destructuring, some macro-writing macros are defined, in particular, a hygienic procedural define-macro and a procedural version of syntax-rules, named macro-rules. The latter is almost as easy to use as syntax-rules, but much more powerfull. Here is its syntax

[syntax] (macro-rules sym ... (key ...) (pat (where fender ...) .. tpl) ....)

Note the special use of dots here and below: Three dots are ellipses, as usual, i.e. the pattern on the left is repeated zero or more times, two dots, zero or one time, 4 dots one ore several times.

This form can be used instead of syntax-rules in define-syntax, let-sytax and letrec-syntax, provided, you import it for-syntax. sym ... denote the injected symbols to break hygiene (if there is none, the constructed macro is hygienic). key ... and pat .... symbols are as in syntax-rules, fender ... are expressions on pattern variables which must pass for the pattern to match, and tpl .... are usually quasiquoted expressions.

And here is the syntax of define-macro

[syntax] (define-macro (name . args) (where fender ...) .. xpr ....))

This macro does not handle injected or keyword symbols. For this use macro-rules.

The implementation of these macros relies heavily on two routines of Paul Graham's macro bible "On Lisp, p. 232", which he used to implement dbind, a variant of Common Lisp's destructuring-bind. They are called destruc and dbind-ex and evaluated at compile-time. Here is a Scheme version for lists only:

(define (destruc pat seq)
  (let loop ((pat pat) (seq seq) (n 0))
    (if (pair? pat)
      (let ((p (car pat)) (recu (loop (cdr pat) seq (+ n 1))))
        (if (symbol? p)
          (cons `(,p (list-ref ,seq ,n)) recu)
          (let ((g (gensym)))
            (cons (cons `(,g (list-ref ,seq ,n))
                        (loop p g 0))
                  recu))))
      (let ((tail `(list-tail ,seq ,n)))
        (if (null? pat)
          '()
          `((,pat ,tail)))))))
(define (dbind-ex binds body)
  (if (null? binds)
    `(begin ,@body)
    `(let ,(map (lambda (b) (if (pair? (car b)) (car b) b))
                binds)
       ,(dbind-ex
          (mappend (lambda (b) (if (pair? (car b)) (cdr b) '()))
                   binds)
          body))))
(define (mappend fn lists)
  (apply append (map fn lists)))

Graham's code works as follows: First, destruc traverses the pattern and groups each symbol with the location of a runtime object, using gensyms to step down the pattern while grouping the gensym bound object with all pairs depending on this gensym. So, for example, (destruc '(a (b . c) . d) 'seq) will result in

((a (list-ref seq 0))
 ((#:g (list-ref seq 1)) (b (list-ref #:g 0)) (c (list-tail #:g 1)))
 (d (list-tail seq 2)))

This tree is then transformed via dbind-ex into a nested let to produce dbind's result

(let ((a (list-ref seq 0))
      (#:g (list-ref seq 1))
      (d (list-tail seq 2)))
  (let ((b (list-ref #:g 0))
        (c (list-tail #:g 1)))
    body))

Contrary to the bindings library, this library doesn't use generic sequences. Indeed, for macro-writing macros lists are sufficient, I think. But off course, I have to provide some extensions to Graham's code, length checks, wildcards, non-symbol literals, as in the bindings egg. wildcards and nonsymbol literals bind nothing, the former matching anything, the latter only themselfs.

The last feature missing is fenders, which is important in particular for macro-rules and can easily be implemented with a where clause: A pattern matches successfully if only each pattern variable can be bound and the where clause is satisfied. If the where clause doesn't pass the next pattern is tried.

The module procedural-macros

procedural-macros

[procedure] (procedural-macros sym ..)

documentation procedure. Shows the exported symbols and the syntax of such an exported symbol, respectively.

macro-rules

[syntax] (macro-rules sym ... (keyword ...) (pat (where fender ...) .. tpl) ....)

like syntax-rules, but the templates are usually quasiquote-expressions. Moreover, the symbols sym ... are injected, if there are any.

Note, that non-symbol literals are accepted in each pat and considered a match if they are equal to the evaluation of a corresponding expression in the macro-code.

macro-rules must be imported for-syntax if used in the preprocessing phase of a macro evaluation. The same applies to bind and friends.

define-macro

[syntax] (define-macro (name . args) (where fender ...) .. xpr ....))

generates a hygienic implicit-renaming macro, name.

macro-let

[syntax] (macro-let (((name . args) (where fender ...) .. xpr ...) ...) body ....)

evaluates body ... in the context of parallel hygienic macros name ....

macro-letrec

[syntax] (macro-letrec (((name . args) (where fender ...) .. xpr ...) ...) body ....)

evaluates body ... in the context of recursive hygienic macros name ....

once-only

[syntax] (once-only (x ...) body ....)

to be used in a macro-body to avoid side-effects. The arguments x ... are only evaluated once. once-only must be imported for-syntax.

with-gensyms

[syntax] (with-gensyms (x ...) . body)

to be used in a macro body. Generates a list of gensyms x ... with-gensyms must be imported for-syntax.

Requirements

None

Usage

(use procedural-macros)
(import-for-syntax (only procedural-macros macro-rules with-gensyms once-only))

Examples


(use procedural-macros)
(import-for-syntax
  (only procedural-macros macro-rules with-gensyms once-only))

;; two anaphoric macros
(define-syntax aif
  (macro-rules it ()
    ((_ test consequent)
     `(let ((,it ,test))
        (if ,it ,consequent)))
    ((_ test consequent alternative)
     `(let ((,it ,test))
        (if ,it ,consequent ,alternative)))))

(define-macro (alambda args xpr . xprs)
  (inject self)
  `(letrec ((,self (lambda ,args ,xpr ,@xprs)))
     ,self))

;; effective membership testing
(define-macro (in? what equ? . choices)
  (let ((insym 'in))
    `(let ((,insym ,what))
       (or ,@(map (lambda (choice) `(,equ? ,insym ,choice))
                  choices)))))

;; verbose if
(define-syntax vif
  (macro-rules (then else)
    ((_ test (then xpr . xprs))
     `(if ,test
        (begin ,xpr ,@xprs)))
    ((_ test (else xpr . xprs))
     `(if ,(not test)
        (begin ,xpr ,@xprs)))
    ((_ test (then xpr . xprs) (else ypr . yprs))
     `(if ,test
        (begin ,xpr ,@xprs)
        (begin ,ypr ,@yprs)))))

;; procedural version of cond
(define-syntax my-cond
  (macro-rules (else =>)
    ((_ (else xpr . xprs))
     `(begin ,xpr ,@xprs))
    ((_ (test => xpr))
     (let ((temp test))
       `(if ,temp (,xpr ,temp))))
    ((_ (test => xpr) . clauses)
     (let ((temp test))
       `(if ,temp
          (,xpr ,temp)
          (my-cond ,@clauses))))
    ((_ (test)) `(if #f #f))
    ((_ (test) . clauses)
     (let ((temp test))
       `(if ,temp
          ,temp
          (my-cond ,@clauses))))
    ((_ (test xpr . xprs))
     `(if ,test (begin ,xpr ,@xprs)))
    ((_ (test xpr . xprs) . clauses)
     `(if ,test
        (begin ,xpr ,@xprs)
        (my-cond ,@clauses)))))

;; procedural version of letrec
(define-macro (my-letrec var-val-pairs . body)
              (where ((list-of (bindable? (var val)))
                      var-val-pairs))
  (let ((vars (map car var-val-pairs))
        (vals (map cadr var-val-pairs))
        (aux (map (lambda (x) (gensym)) var-val-pairs)))
    `(let ,(map (lambda (var) `(,var #f)) vars)
       (let ,(map (lambda (a v) `(,a ,v)) aux vals)
         ,@(map (lambda (v e) `(set! ,v ,e)) vars vals)
         ,@body))))

(my-letrec ((o? (lambda (m) (if (zero? m) #f (e? (- m 1)))))
            (e? (lambda (n) (if (zero? n) #t (o? (- n 1))))))
           (list (o? 95) (e? 95)))

;; local macros
(letrec-syntax (
     (sec (macro-rules ()
               ((_ lst) `(car (res ,lst)))))
     (res (macro-rules ()
             ((_ lst) `(cdr ,lst))))
     )
     (sec '(1 2 3)))
;-> 2

(macro-letrec (
     ((sec lst) `(car (res ,lst)))
     ((res lst) `(cdr ,lst))
     )
     (sec '(1 2 3)))
;-> 2

(macro-let (
     ((fir lst) (where (list? lst)) `(car ,lst))
     ((res lst) (where (list? lst)) `(cdr ,lst))
     )
     (fir (res '(1 2 3))))
;-> 2

;; non-symbolic literals
(define-syntax foo
  (macro-rules ()
    ((_ "foo" x) x)
    ((_ #f x) x)
    ((_ a b) (where (string? a)) `(list ,a ,b))
    ((_ a b) (where (odd? a)) `(list ,a ,b))
    ((_ a b) a)))
(foo "foo" 1)
; -> 1
(foo "bar" 2)
; -> '("bar" 2)
(foo #f 'blabla)
; -> 'blabla
(foo 1 2)
; -> '(1 2)
(foo 2 3)
; -> 2

(define-syntax add
  (macro-rules ()
    ((_ x y) (where (string? x) (string? y))
     `(string-append ,x ,y))
    (( _ x y) (where (integer? x) (integer? y))
     `(+ ,x ,y))))
(add 1 2)
;-> 3
(add "x" "y")
;-> "xy"

Last update

Nov 25, 2015

Author

Juergen Lorenz

License

Copyright (c) 2015, Juergen Lorenz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
Neither the name of the author nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission. 
  
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Version History

1.0.1
bug fix in literals
1.0
initial import