Jun Inoue
jun.l****@gmail*****
2006年 4月 27日 (木) 17:37:59 JST
ヤマケンさんの立場はよくわかりました。それなら文句ありません。 compaction を終えたら暫くは離れるつもりなので、macro の現状をちょっと 説明しておきます。 > 一方、SRFIについては必要性が無い限りCで実装する事には積極的に反 > 対です。今までに書かれたmodule-srfi*.cはSchemeでは書けなかったか、 > pure Schemeでは著しく効率が落ちるものだけです。SRFI-34もCで書き > たいわけではなく、実装時点でマクロを利用できなかったからああなっ > ただけです。 > > 太田さんが書きかけのmodule-srfi1.cは上記に当てはまりませんが、 > uimではSLIB実装の方を使うつもりである事は最初期から繰り返し表明 > しています。効率のために一部のprocedureをCで独自実装する事は考え > られますが。 > > 井上さんがマクロを実装してくれたおかげで既存のSchemeによるSRFI実 > 装を流用できる幅が大きく拡がったわけですが、今あるmodule-srfi*.c > は効率面重視の選択肢としてそのまま併存させるつもりです。一定の品 > 質も確保できていると考えているし、マクロはいらないがSRFI-8は欲し > いというような場面もあると思うので。 Macro は効率最悪です。実効速度は相対的に結構速い方ですが、そもそも要求 されてる操作自体がかなり高価な上に、cell をバンバン消費します。 Template に出現する pair と vector はほぼ全部、展開回数 + compile 回数 だけ copy されます。同じ所を複数回実行すればそれだけ展開し直されるので、 hot spot で macro を使うのは自殺行為です。 この impact を多少和らげる方法として source list に破壊的に memoize す ることが考えられます。展開回数が一回と指定されてる伝統的 macro を実装 するなら、これをするのは必須です。*多分* 普通に scm_eval() で代入する だけでいけると思いますが、何か見落としてそうな気配がします。今のところ 気づいている問題点・注意点は 1) 展開結果が pair (list) でないときは memoize 不可。(ENTYPE すること になるから。storage-compact では ENTYPE は基本的に使えない。) 2) Eval で分岐が増える。痛い。→macro を使う部分だけに対価を払わせる形 で元の form を渡す手段は無いものか 3) 今のところ expander が返す object は完全に新しく allocate されたも のであることが保証されている。TR_* みたいなものでこの保証を崩すならば memoization で破壊されうる箇所 (operator 位置の pair) だけは copy され ていることを確認しないといけない。ただし TR_* で cell をケチろうとして も、普通の template は symbol だらけ = far symbol に置き換える必要のあ る container だらけなので、あんまり得ではない。 4) scm_p_eval() の入力を deep copy する必要あり。あるいは eval! が仕様 だと言い張る。 5) syntax-case と共存不可能かもしれない。 (define f (let-syntax ((macro (lambda () (syntax-case [...])))) (lambda args (macro some-args)))) (f 0) (f 1) 一回目の呼び出しで args = (0) な環境を捕捉して、その条件の元で展開され た form が代入されるので、二回目の呼び出しでも (macro some-args) のあっ た箇所は args = (0) な環境で評価されてしまう。 syntax-rules だと、環境の中身は無視して frame の数を覚えるだけでいいの で、この問題は起きない。一方、syntax-case などの unhygienic macro だと、 定義により env を辿っていくだけでは見つけられない環境を参照させられる 可能性があるので、env *object* を捉えるか、文字どおり rename をしない といけない。後者は SigScheme では無理。ただ、伝統的 macro では正に env object を掴むのが求められている semantics なので、問題なし。 syntax-case の環境捕捉がそれより柔軟だとまずい。 Memoization の他に選択肢の成り損ねとしては: a) 引数 list の address を key にして hash table に格納する → Tight loop の中で macro を呼ぶなら恩恵はある。その代わり table 自身 が memory を食うのと、展開前の form を捨ててないので節約度は破壊代入よ りは低い。eval を使わないならね。 b) Far symbol を read 可能にして uim compile 時に片っ端から展開する →話にもなりません。Gauche とか bigloo 使った方が早いし、展開する macro としない macro を見分けられません。 それと、展開された後に残る wrapped identifier の lookup はやや遅くなり ます。 もう一つ。 (let-syntax ((macro ([...]))) macro) みたいにして macro を本来の scope の外に放り出して使ったり、 (let-syntax ((macro (let ((a 0)) (syntax-rules [...])))) (macro foo)) みたいに let/define-syntax と syntax-rules の間に binding form を挟ん だりすると scheme level で予測不能動作をします。Crash はしないと思いま すが。名前空間を分けた方がいいかもしれませんが、多分高くつきます。