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