Schemeのマクロ

define-syntaxで定義するSchemeのマクロ(Hygienic=健全な、参照上透過的)は最初理解できなかったけど、慣れるとなかなか楽しい。

define-syntaxは式の変形パターンを指定すると勝手にパターンマッチングをやってくれる。ということでいくつか例を挙げてみる。

  • 簡単な例
(define-syntax 1-
  (syntax-rules ()
    ((_ %n)
     (- %n 1))))
  • 少し複雑な例
(define-syntax if=>
  (syntax-rules ()
    ((_ %test-form %then-form %else-form)
     (cond (%test-form => %then-form)
           (else %else-form)))
    ((_ %test-form %then-form)
     (if=> %test-form %then-form (if #f #f)))))

Web上ではまとまった説明が見つからなかったので、最初理解するのに苦労したんだけど、"_"は「何でも良い」ことを表しているらしい(パターンの先頭はマクロ名に決まっているので、マクロ名の代わりとして使われることが多い)。syntax-rulesは最初はただのおまじないだと思っていても問題ない。パターン変数の先頭に%を付けているのは特に文法的な意味はなくて、単に読む人に分かりやすくするため。一応パターン(あるいはプレースホルダ)の頭文字pを意識したつもり。

で、ポイントはこれがリストの変形だということ。だから文字列やシンボルを作り出したりはできない。どういうことかというと、

(a b c) => (c (b (a)))

こういう変形はできるけど、

("a" "b") => "ab"

こういう変形とか、

(+S -V) => ((S V) (+ -))

こういう変形はできない(こういうことがしたければ「伝統的な」マクロを使う)。

最初展開の仕方をトップダウンに考えてなかなかうまくいかなかったのが、展開された形から考えることで割とすんなり理解することができた。あと、複雑なマクロの場合は状態遷移の考え方が必要になる。

define-syntaxはturing completeなので理論的にはあらゆる計算ができる。というわけで、遊びでtaraiのマクロ版を作ってみた。

(define-syntax tarai-mac
  (syntax-rules ()
    ; 初期状態
    ((_ %x %y %z)
     (tarai-mac %x %y %x %y %z))
    ; (< x y) を計算中
    ((_ (%x1 %x2 ...) (%y1 %y2 ...) %x %y %z)
     (tarai-mac (%x2 ...) (%y2 ...) %x %y %z))
    ; x < y の場合
    ((_ () (%r1 %r2 ...) %x %y %z)
     (%y))
    ; x >= y の場合
    ((_ (%r1 ...) () (%x1 %x2 ...) (%y1 %y2 ...) (%z1 %z2 ...))
     (tarai-mac (tarai-mac (%x2 ...) (%y1 %y2 ...) (%z1 %z2 ...))
                (tarai-mac (%y2 ...) (%z1 %z2 ...) (%x1 %x2 ...))
                (tarai-mac (%z2 ...) (%x1 %x2 ...) (%y1 %y2 ...))))
    ))

tarai自体は次のような関数で、とにかく無駄な計算が多いのでベンチマークや遅延計算の例によく使われるもの。

(define (tarai x y z)
  (if (< x y)
      y
      (tarai (tarai (1- x) y z)
             (tarai (1- y) z x)
             (tarai (1- z) x y))))

KAWAにはmacroexpandがないのでGaucheでの実行例を示すと、

(macroexpand '(tarai-mac (() ()) (() () ()) (())))
==> ((() () ()))

一応動いている模様。(追記:全然ダメ。この例だとたまたま動いてるだけ)

と、ここまでが前フリ(長いな)…というのは冗談だけど、少なくともtaraiの例を出したのはこのため。

KAWAにはmacroexpandがない、ならば作ってしまえということで、ゴニョゴニョやったらできた。

  • macroexpand1.java:
package kawa.lang;
import kawa.standard.*;
import gnu.mapping.*;
import gnu.expr.*;
import gnu.lists.*;

public class macroexpand1 extends Procedure1 {
    private static final String className = "kawa.lang.macroexpand1";

    public static void register(String name)
    {
        Language lang = Language.getDefaultLanguage();
        lang.define(name, new AutoloadProcedure(name, className, lang));
    }

    private Translator tr;

    public macroexpand1()
    {
        tr = (Translator) Compilation.getCurrent();
    }

    Object expandMacro(Macro macro, Object obj)
    {
        Syntax saveSyntax = tr.currentSyntax;
        tr.currentSyntax = macro;

        Compilation saveCompilation = Compilation.getCurrent();
        Compilation.setCurrent(tr);

        try {
            obj = macro.expand(obj, tr);
        } finally {
            tr.currentSyntax = saveSyntax;
            Compilation.setCurrent(saveCompilation);
        }

        return obj;
    }

    Object expandForm(Object form, Object obj)
    {
        if ((form instanceof String) ||
            (form instanceof Symbol)) {
            Object val = Environment.getCurrent().get(form);
            if (val instanceof Macro) {
                Macro macro = (Macro) val;
                return expandMacro(macro, obj);
            }
        }
        return obj;
    }

    public Object apply1(Object arg1)
    {
        if (arg1 instanceof Pair) {
            Object car = ((Pair) arg1).car;
            if (car instanceof SyntaxForm) {
                SyntaxForm sf = (SyntaxForm) car;

                ScopeExp saveScope = tr.currentScope();
                tr.setCurrentScope(sf.scope);
                try {
                    return expandForm(sf.form, arg1);
                } finally {
                    tr.setCurrentScope(saveScope);
                }
            } else {
                return expandForm(car, arg1);
            }
        }
        return arg1;
    }
}
  • macro.scm:
(define (macro? expr)
  (instance? expr <kawa.lang.Macro>))

(define (syntax? expr)
  (instance? expr <kawa.lang.Syntax>))

(define (identifier? expr)
  (and (instance? expr <kawa.lang.SyntaxForm>)
       (kawa.lang.SyntaxForm:identifier? expr)))

(kawa.lang.macroexpand1:register "%macroexpand-1")

(define (macroexpand-1 form)
  (%macroexpand-1 form))

(define (macroexpand form)
  (let ((exp (%macroexpand-1 form)))
    (if (eq? form exp)
        form
        (macroexpand exp))))

動作確認:

% java kawa.repl
#|kawa:1|# (load "macro")
#|kawa:2|# (%macroexpand-1 '(tarai-mac (() ()) (() () ()) (())))
(#<syntax tarai-mac in #89> (()) (() ()) (() ()) (() () ()) (()))
#|kawa:3|# (macroexpand-1 '(tarai-mac (() ()) (() () ()) (())))
(#<syntax tarai-mac in #91> (()) (() ()) (() ()) (() () ()) (()))
#|kawa:4|# (macroexpand '(tarai-mac (() ()) (() () ()) (())))
((() () ()))
#|kawa:5|#

これでKAWAでもマクロをバリバリに使ったコードがデバッグできる。

追記

夜中のハイなテンションで一気に作業したから確認が適当だった。taraiマクロ版は別の例だとエラーになったり無限ループしたりとバグだらけ。そもそもあの例だとtaraiの後半の再帰計算に入らないから意味ないじゃん。