Anaphoric If

Arc の aif のように、意図的な変数キャプチャ (マクロ定義の中で作られた変数をマクロユーザーが参照できるようにすること) を用いたマクロを作る時、Scheme では一般に datum->syntax という関数を使います。

(define-syntax (aif stx)
  (syntax-case stx ()
    ((aif expr then else)
     (with-syntax ((it (datum->syntax #'aif 'it)))
       #'(let ((it expr))
           (if it then else))))))

datum->syntax の第1引数で、第2引数が有効になる範囲を指定します (ここでは aif 構文内)。なお、この場合 syntax-rules ではなく syntax-case で定義する必要があります。

これで、

> (aif (assq 'a '((a . 1) (b . -2)))
       (cdr it)
       0)
1

it によって (assq...) の値が参照できるようになるわけです。

さて、こちらは PLT Scheme のコミュニティーで知られている方法なんですが、

(require scheme/stxparam)

(define-syntax-parameter it
  (lambda (stx)
    (raise-syntax-error #f "can only be used inside anaphora" stx)))

(define-syntax aif
  (syntax-rules ()
    ((aif expr then else)
     (let ((val expr))
       (if val
           (syntax-parameterize ((it
                                  (make-rename-transformer #'val)))
             then)
           else)))))

構文パラメータというものによって、syntax-case や datum->syntax を使わずに特別な識別子を導入することができます。

この aif に基づいて、派生マクロが以下のようにいとも簡単に書けます。

(define-syntax awhen
  (syntax-rules ()
    ((awhen expr . body)
     (aif expr (begin . body) (void)))))

(define-syntax aand
  (syntax-rules ()
    ((aand) #t)
    ((aand expr) expr)
    ((aand expr . rest)
     (aif expr (aand . rest) #f))))

(define-syntax acond
  (syntax-rules (else)
    ((acond) (void))
    ((acond (else . body))
     (begin . body))
    ((acond (expr) . rest)
     (or expr (acond . rest)))
    ((acond (expr . body) . rest)
     (aif expr (begin . body) (acond . rest)))))

構文パラメータを使わないバージョンの aif ではこれは不可能です。上述の datum->syntax の第1引数云々の事情により、it が有効になる場所が元の aif 構文の中だけに限定されてしまうため、派生構文の中では it が使えないのです (実際には不可能ではないらしいんですが、分かりにくい方法なので紹介しないでおきます)。


さてここで、前回作った Arc の角括弧構文が使えるようにしてみましょう。モジュールを跨いで構文を拡張するのは若干気が引けますが

(require (only-in "arcfun.ss" make-brackets-funny))
(make-brackets-funny aif awhen aand acond)
> (map [aand (cdr _) (abs it)]
       '((a . 1) (b . -2) (c . #f)))
(1 2 #f)

あっさり出来ました。



追記:

モジュールを跨いでマクロを拡張するのは云々という不安の背景について、少し技術的な補足をいたします。

カリー化関数構文の部分評価の処理において、local-expand を使って式をフルに展開している箇所があります。

コードの分析のため、核構文のみの形にする必要があるからなんですが、そうすると、マクロ展開された式の中に、モジュールでエクスポートされていない識別子やローカル変数などが出てくる場合があるんです。

マクロが定義されているモジュールの外からそのような識別子にアクセスされることは、モジュールの安全性を脅かす事態です。というわけで、MzScheme では syntax certificate というものを問題のある識別子に付与し、アクセス権限の無い文脈でのアクセスがあるとコンパイル・エラーを発する仕組みになっています。

これが実は悩みの種で、以前の実装では、例えば fun 構文の中で aif 構文を使うと

compile: reference is more certified than binding in: val

のようなエラーが出てしまっていました。val は anaphora モジュールの中の aif 構文の定義に出てくるローカル変数であるため、サードパーティーのマクロ (まぁどっちも自分で作ってるんですが) からのアクセスが許可されないわけです。

悩み抜いた結果、ようやく解決方法が分かったのでここに書いておきます。

(define (collect-certs stx)
  (let loop ((stx stx) (certs '()))
    (cond ((syntax? stx)
           (loop (syntax-e stx) (cons stx certs)))
          ((pair? stx)
           (loop (cdr stx) (loop (car stx) certs)))
          (else certs))))

(define (stx-recertify stx)
  (let ((certs (collect-certs stx))
        (insp (current-code-inspector)))
    (lambda (stx)
      (let loop ((certs certs) (stx stx))
        (if (null? certs)
            stx
            (loop (cdr certs)
                  (syntax-recertify stx (car certs) insp #f)))))))

(define (stx->tree stx stop? (certify values))
  (let loop ((stx stx))
    (cond ((stop? stx) (certify stx))
          ((syntax? stx)
           (loop (syntax-e stx)))
          ((pair? stx)
           (cons (loop (car stx))
                 (loop (cdr stx))))
          (else stx))))

collect-certs により、expand して得られた構文オブジェクト (stx) に含まれる全ての syntax certificate を集めます。保護された識別子だけでなく、それを包む式に付与されたものも含めてです。実際に集めているのは構文オブジェクトなんですが、それに certificate が含まれています。

そして stx-recertify によって、個別に取り出された識別子に対し、集められた certificate を全て与えてしまいます (syntax-recertify)。

呼び出し方

(stx->tree expanded-syntax
           identifier?
           (stx-recertify expanded-syntax))

後はツリーのトラバーサル関数を使って自由にコード変形をすることができます。

一般に、1つのコードには複数のモジュールに由来するマクロの呼び出しがあるものなので、識別子の出自に関わらず全ての certificate を付与してしまうこの方法は少々乱暴な気もするんですが、エラーが出なくなったので取り敢えず良しとしておきます。


ファイル:
anaphora.ss arcfun.ss hasfun-helper.ss mlfun.ss


参考文献:
On Lisp - Anaphoric Macros
PLT Scheme Blog - Dirty Looking Hygiene