Scheme:er-macro関連
er-macro について、調べた内容のメモ等です。
hamayama(2018/11/15 10:42:48 UTC)(2018/11/17 23:02:03 UTC)
(2018/11/22 13:45:36 UTC)
er-macro の symbol 挿入の問題?
よく理解してはいませんが、以下の内容はすべて関係がある気がする。。。
[1] Implement datum->syntax for explicitely renamed identifiers
https://github.com/ashinn/chibi-scheme/pull/496
[2] comp.lang.scheme: The power of unhygienic macros: syntax-case vs. er-macro-transformer/sc-macro-transformer
https://groups.google.com/forum/#!topic/comp.lang.scheme/2gFSbX-Wcy4
[3] Gauche:Translation:Devlog:マクロシステム拡張
https://practical-scheme.net/wiliki/wiliki.cgi?Gauche%3ATranslation%3ADevlog%3A%E3%83%9E%E3%82%AF%E3%83%AD%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%8B%A1%E5%BC%B5
[4] .mjtの日記復帰計画: [scheme][mosh] explicit-renaming macroの拡張
http://d.hatena.ne.jp/mjt/20110302/p1
[5] wasabizの日記: 健全なマクロは壊れている (リンク切れ)
http://wasabiz.hatenablog.com/entry/2014/09/28/145739
エラーになるケース (Gauche 0.9.7_pre2)
(define-syntax divide (er-macro-transformer (lambda (expr rename compare) (let ((x (list-ref expr 1)) (y (list-ref expr 2))) `(,(rename 'if) (,(rename 'zero?) ,y) (failure) (,(rename '/) ,x ,y)))))) (define-syntax wrapper (syntax-rules () ((wrapper) (let ((failure (lambda () (display "division by zero") (newline)))) (divide 1 0))))) (let () (wrapper)) ;; *** ERROR: unbound variable: failure ;; Stack Trace: ;; _______________________________________ ;; 0 (failure) ;; [unknown location] ;; 1 (eval expr env) ;; at "C:\\Program Files (x86)\\Gauche\\share\\gauche-0.97\\0.9.7_pre2\\lib ;; /gauche/interactive.scm":267
syntax-rules の方を er-macro に直すといけるようだが。。。
(define-syntax divide (er-macro-transformer (lambda (expr rename compare) (let ((x (list-ref expr 1)) (y (list-ref expr 2))) `(,(rename 'if) (,(rename 'zero?) ,y) (failure) (,(rename '/) ,x ,y)))))) (define-syntax wrapper (er-macro-transformer (lambda (expr rename compare) (let () `(,(rename 'let) ((failure (,(rename 'lambda) () (,(rename 'display) "division by zero") (,(rename 'newline))))) (,(rename 'divide) 1 0)))))) (let () (wrapper)) ;; division by zero
齊藤 (2018/11/18 01:54:39 UTC): ↑ syntax-rules によって新しく導入される名前 (この場合は faulure) は実質的にリネームされることになっているので自然な挙動のように思えます。
hamayama(2018/11/18 11:36:29 UTC): うーむ。syntax-rules 側の「failure をリネームした何か」を、
R6RS の仕組みを追加することで、er-macro 側でも参照可能にしようということなのかな?
hamayama(2018/11/22 13:45:36 UTC): 正常ケース
Larceny v1.3 (larceny.bat -r7rs) ではこれもエラーになる (datum->syntax が必要とのこと([2]のコメント3より))。
(cond-expand (larceny (import (scheme base) (scheme write) (rename (explicit-renaming) (er-transformer er-macro-transformer)))) (else)) (define-syntax divide (er-macro-transformer (lambda (expr rename compare) (let ((x (list-ref expr 1)) (y (list-ref expr 2))) `(,(rename 'if) (,(rename 'zero?) ,y) (failure) (,(rename '/) ,x ,y)))))) (let ((failure (lambda () (display "division by zero") (newline)))) (divide 1 0)) ;; division by zero
hamayama(2018/11/22 13:45:36 UTC): メモ。syntax-rules に failure を引数として渡せばいける。
(define-syntax divide (er-macro-transformer (lambda (expr rename compare) (let ((x (list-ref expr 1)) (y (list-ref expr 2))) `(,(rename 'if) (,(rename 'zero?) ,y) (failure) (,(rename '/) ,x ,y)))))) (define-syntax wrapper (syntax-rules () ((wrapper failure) (let ((failure (lambda () (display "division by zero") (newline)))) (divide 1 0))))) (let () (wrapper failure)) ;; division by zero
hamayama(2018/11/22 13:45:36 UTC): R6RS の syntax-case を使えば動作する (Chez Scheme v9.5, Sagittarius v0.9.4)。
(define-syntax divide (lambda (stx) (syntax-case stx () ((k x y) (with-syntax ((failure (datum->syntax #'k 'failure))) #'(if (zero? y) (failure) (/ x y))))))) (define-syntax wrapper (syntax-rules () ((wrapper) (let ((failure (lambda () (display "division by zero") (newline)))) (divide 1 0))))) (let () (wrapper)) ;; division by zero
hamayama(2018/11/22 13:45:36 UTC): 結局、[1][2]で言っていることは、
「er-macro でライブラリを書くと、ユーザが syntax-rules でラップしたときにエラーになるが、
同じライブラリを R6RS の syntax-case で書くと、ユーザが syntax-rules でラップしてもエラーにならないので、
記述力に差があるのではないか」ということだと思う。
ただ、er-macro の例だけを見ると、それはそうなるよねという感じなので、
R6RS の方が「failure をリネームした何か」を陽に扱えるように、何やら仕様を細かく定めている気がする。
これ以上は、論文等の調査が必要そう (あまり深入りしない方がよい気もするが。。。)。
(検索キーワード: syntax-case portable)
[6] How do I write anaphoric macros in portable scheme?
https://stackoverflow.com/questions/25824466/how-do-i-write-anaphoric-macros-in-portable-scheme
[7] Keeping it Clean with Syntax Parameters
http://www.schemeworkshop.org/2011/papers/Barzilay2011.pdf
齊藤(2018/11/23 11:30:21 UTC): syntax-case はリネームではなく、識別子に文脈情報がくっついている (かのように動作する) という動作モデルだったはずです。
explicit-renaming を基礎にして syntax-case を実装することは出来る (やっている処理系がある) ので、動作モデルが違うものを混在することは出来るのでしょうが、~
レイヤが違う話なのではないかと思います。
いうなれば C とアセンブリを比較しているようなもので、処理系の (この場合は syntax-case の) 実装次第で explicit-renaming と組み合わせたときの挙動が違ってきてもおかしくはないような気がします。
hamayama(2018/11/23 17:27:50 UTC): 気にしていることは、低レベルマクロと高レベルマクロの組み合わせについてで、
例えば、R7RS large に er-macro が入ったとしても、それだけでは機能が足りないのでは? ということです。
なので、er-macro にその機能を追加する方法を知りたいという感じです。
[1][2]によると、そのひとつの方法は、R6RS の datum->syntax を追加することだと言っているようなので、
そのあたりから調べようかと思っています。
(ただ、モデルが違いすぎて、実現が相当難しいようであれば、
あきらめてユーザ側での回避方法(syntax-rulesの引数に名前を渡すようにする等)をまとめるくらいにしようかと思います。。。)
Shiro(2018/11/25 00:21:42 UTC): Hygienic macroに必要なのは、マクロ定義環境とマクロ呼び出し環境を区別することで、er-macroの場合は「マクロ定義環境はリネーム」「マクロ呼び出し環境は生シンボル」という区別でそれを実現しています。R6RSの低レベルマクロのdatum->syntax
は一歩進んで、任意の識別子について紐つけられた環境を取り出せる(別の識別子につけかえられる)ようにしていて、それはhygienic macroの要求仕様の外になりますが、確かにそのままのer-macroには無い機能です。renameした識別子に環境情報をぶらさげておけばいいんですが、マクロ呼び出し環境で参照されることを期待して挿入した生シンボルについては、そのまま持ち運ぶと更なるマクロ呼び出しで環境情報が失われる場合があるので、er-macro展開後に環境情報を付加してやらないとなりません。
hamayama(2018/11/26 07:34:31 UTC): Gauche に datum->syntax を追加してみました。
https://github.com/shirok/Gauche/pull/399
Shiro(2018/11/26 08:26:04 UTC): 上記 datum->syntax
による方法は、もう一段マクロが挟まると動かなくなります (Sagittariusでは"unbound variable failure" でエラー):
(define-syntax divide (lambda (stx) (syntax-case stx () ((k x y) (with-syntax ((failure (datum->syntax #'k 'failure))) #'(if (zero? y) (failure) (/ x y))))))) (define-syntax divide2 (syntax-rules () ((divide2 x y) (divide x y)))) (define-syntax wrapper (syntax-rules () ((wrapper) (let ((failure (lambda () (display "division by zero") (newline)))) (divide2 1 0))))) (let () (wrapper))
確実に動かすためには、divideのユーザがどういう形でfailureを定義したら良いかを意識しなければなりませんが、どっちにせよ意識しないとならないならdivideに引数としてfailureを渡す方が明確でしょう。なので、「divideがどういう状況であっても最終的な展開形におけるレキシカルスコープのfailureを参照する」が目的ならdatum->syntax
は解決にならないと思います。
hamayama(2018/11/26 12:09:26 UTC): うーむ確かに。狙った識別子が存在するマクロ内でしか、動作しないですね。
例えば、マクロ内に (define-record-type typename ...) と書いた場合に、
typename をキーにして、同じマクロ内でのみ typename-prop1 という名前も使えるようにするとか。。。
ただ、ユーザの立場からすると、エラーになったりならなかったりで、むしろ混乱するかもしれませんね。。。
--
hamayama(2018/11/27 13:13:51 UTC): 別のアイデアですが、syntax-rules の方を拡張して、
(define-syntax wrapper (syntax-rules () + (failure) ((wrapper) (let ((failure (lambda () (display "division by zero") (newline)))) (divide2 1 0)))))
のように出力リテラルを追加のリストで指定可能にするのはどうでしょうか。
(プラス記号が拡張のマーカーで、それがなければ今まで通りの syntax-rules とする)
実装自体は、テンプレート展開時に出力を memq でチェックして、
出力リテラルと一致したらリネームをスキップするようにすれば、いけると思います。
ただ、理論的には何か穴がありそうな気もしますが。。。
齊藤(2018/11/27 17:16:01 UTC): syntax-rules の健全性を損なうような拡張はさすがに不格好でしょう。
このような事例では Racket の syntax parameter のような考え方を導入できませんか。
hamayama(2018/11/28 03:25:09 UTC): まあ、そうですよね。。。
やはり、多少数が多くなっても、syntax-rules に明示的に引数で渡すのがよさそうですね。
([1][2]の議論の元は、SRFI 99 の define-record-type が生成する名前を
syntax-rules で扱おうとしてエラーになったということのようです)
または、低レベルマクロで書くか (でも、低レベルマクロは互換性が低そうなのですよね。。。)。
syntax-parameter は、ちょっとすぐには理解できない感じなので、また別途調べてみます。
--
hamayama(2018/11/30 09:47:38 UTC): メモ。伝統的マクロのよくない例として、以下のようなものがある。
(define-macro (my-when test then . rest) `(if ,test (begin ,then ,@rest))) (my-when 1 'OK) ; => OK (let ((if 1)) (my-when 1 'OK)) ; => error
my-when をライブラリとして実用しようとした場合、
「(A) my-when は、内部で if と begin を使用しているため、
これらを別のものに束縛していた場合には、正常に動作しません。」
というような注意書きが、リファレンスに必要になると思う。
一方、上記の divide2 の例はどうか。
「(B) divide を使う場合、記述した divide から作られる識別子と同じマクロ展開環境に
ある failure が使用されます。このため、他のマクロ展開環境の場所に failure を記述しても、
divide 用の failure とは認識されません。」
というような注意書きが、リファレンスに必要になると思う。
(他には、例えば、[1] のコメントにある (define-record-type record #t #t) の例だと、
define-record-type と record のどちらがアクセサ挿入箇所のキーになるのか、等)
これらは、注意書きがなかった場合に、
「普通に使っていると、あまり問題にならない」
「問題となる条件がそろった場合に、ユーザ側のプログラムを見ても、原因が分からない」
という意味で、同じくらい まずいことのような気がする。。。
R6RS には datum->syntax があり、R7RS の Chibi Scheme や Gauche には、
生シンボルを挿入できる er-macro がある ([4]に関連する記述あり)。
これらは、それぞれ、上記の (B) と (A) の問題を発生させうる要因となる。
(A) と (B) は独立した問題かもしれないが、Scheme を使うユーザ側としては、
ライブラリを使おうとした場合に、そのライブラリの内部実装を気にする要因になると思う。
しかし、これらの機能がなければ、define-record-type のようなアクセサ名を自動生成する機能や
import で prefix を付加するような機能がうまく実現できないのであれば、
やはり(少なくともどちらかは)必要な機能なのだろう。。。
齊藤(2018/11/30 15:08:27 UTC): 最初の例を syntax parameter を用いて書くとこうなります。
#lang racket (require racket/stxparam) (define-syntax-parameter failure (syntax-rules ())) (define-syntax divide (syntax-rules () ((_ x y) (if (zero? y) (failure) (/ x y))))) (define-syntax wrapper (syntax-rules () ((wrapper) (let ((t (lambda () (display "division by zero") (newline)))) (syntax-parameterize ((failure (syntax-rules () ((_)(t))))) (divide 1 0)))))) (let () (wrapper))
このとき divide と wrapper の中にある failure という識別子は共にトップレベルで syntax parameter として定義した failure のことを参照しているから機能するので、もし wrapper を別モジュールに分けて failure を見えなくしたならばエラーになります。
単に同じ名前というだけでは divide 用の failure にすることは出来ず、 divide 用の failure と (free-identifier=? 的に) 同じ識別子であることが要求されます。
failure が syntax parameter として定義されるのでカスタマイズポイントであるというのがわかりやすくなり (ドキュメントには syntax parameter であると書けば細かい説明は省ける) ますし、 explicit-renaming と syntax-rules の組み合わせでも挙動に曖昧さが生じ難いのではないかと考えます。
要するに、 parameter の syntax 版です。
- hamayama(2018/12/02 14:06:44 UTC): ありがとうございます。だいぶイメージができました。
SRFI-139 の 参照実装をみると、syntax-parameterize の実装には dynamic-wind が使われているようですね。
低レベルマクロで出力を組み立てている途中に call/cc で脱出しちゃった場合等も考慮が必要なのかな?
(ちょっとまだ仕様があいまいな気もしますが。。。)