Gauche:#<syntax>や#<macro>の評価
Gauche:WishListより移動
eval内で、macroやspecial formの実体を、macroやspecial formとして扱ってほしい
nekoie(2008/02/10 19:49:45 PST): ややっこしいのですが、やりたい事は、
(let ((lambda-entity lambda)) ((eval `(,lambda-entity (arg) arg) (interaction-environment)) 123)) => 123
のように、evalに、macroやspecial formの「実体」を渡しても機能してほしいです。 (guileはこの挙動をします。mzschemeではそもそもmacro等の実体にさわった時点でエラー。) (Gaucheでは、lambdaの実体がspecial formとして認識されず、(arg)が先に評価され、 エラーになります。)
Gaucheでは、「if」等のspecial formやmacroも、 「if自体はシンボルで、それ自体を(括弧つけずに)評価すると、何らかの実体が得られ」ます。 この「実体」は、普通にSchemeコードを書いている時にはほぼ使われないようです。 というのは、コードとして書かれたS式は、 src/compile.scm(gauche.internalモジュール)のcompile手続きによって、一旦 内部表現に変換されてから実行されるのですが、その際に、 マクロは展開されて消滅し、special formは引数と共に専用のインストラクションコードに 展開されるからです(多分)。
この内部表現への変換は、マクロ展開時と同じタイミングで行われているので、
(let ((if2 if)) (if2 #t 'ok (print "not printed")))
のような事はできません("not printed"がprintされた後にエラーになる)。
- あとでguileで試したら、okが返ってきた……。
- どうもguileでは、letがspecial formで、let展開→ifの実体展開→special formとしてのif及びその引数の解釈、という順で動いていて、okが返るっぽいです。言いたい事は多分伝わってると思うのですが、下のfor-eachの例の方も見てみてください。
しかし、evalでは、またS式→内部表現への変換のフェーズから行われるので、
(let ((if2 if)) (eval `(,if2 #t 'ok (print "not printed")) (interaction-environment)))
が機能しても問題ない気がしますし、実際にguileでは機能します。 これはeval内でのみの挙動(というか、compile手続きによる挙動)なので、
(for-each (lambda (z) (z #t 'ok (destroy-the-world))) `(,if))
のような時にはこれまで通り、エラーとしていいと思います。
- こっちはguileで試してもエラーでした。
- と思ったら、エラーなのはコードのミスが原因でした……。↑のコードを訂正しました。そしたらguileだと動いてしまった。詳細は下の方に書きます。
http://d.hatena.ne.jp/ranekov/20080211 にもメモがあります。
とりあえずパッチを書きました。
--- src/compile.scm.orig 2008-02-11 06:26:18.000000000 +0900 +++ src/compile.scm 2008-02-11 07:33:22.000000000 +0900 @@ -1365,7 +1365,10 @@ (module-qualified-variable? head cenv) (let1 mod (ensure-module (cadr head) 'with-module #f) (cenv-lookup (cenv-swap-module cenv mod) - (caddr head) SYNTAX))))) + (caddr head) SYNTAX))) + (and (macro? head) head) + (and (eq? (class-of head) <syntax>) head) + )) ;; pass1 :: Sexpr, Cenv -> IForm ;; @@ -1432,8 +1435,10 @@ (pass1/global-call head)) ((lvar? head) (pass1/call program ($lref head) (cdr program) cenv)) - ((macro? head) ;; local macro + ((macro? head) ;; local macro / macro entity (pass1 (call-macro-expander head program (cenv-frames cenv)) cenv)) + ((eq? (class-of head) <syntax>) ;; syntax entity + (call-syntax-handler head program cenv)) (else (error "[internal] unknown resolution of head:" head))))) (else
- このパッチではpass1/lookup-headを修正しているのですが、pass1/bodyからもpass1/lookup-headが呼ばれている為、internal define関係で問題があるかも知れません。
- 但し、問題が発生するとしても、evalで評価する際のexpr内に、以下の両方が含まれてる場合だけのような気がします。
- exprにinternal defineが含まれている
- exprにspecial formやmacroの「実体」が含まれている(シンボルではなく)
nekoie(2008/02/11 02:11:35 PST): ヤバい。test codeを、
(for-each (lambda (z) (z #t (write "ok") (write "ng"))) `(,if))
のようにしたら、guileで"ng"出力無しに、正常に動いてしまった。
先に内部表現にコンパイルしてしまうGaucheでは、この挙動に変更するのは多分、大改造しないと無理そうな気がする(そもそもこの挙動へと変更する必要があるのかどうかは置いておいて)。
- 「必要があるのか」を考え始めると、上のパッチも利用場面を考えるとかなり怪しいですが……。
このguileの挙動から考えれば、evalで実体を渡せば動くのも当然だったようです。
この部分(syntaxやmacroの実体)は下手に手を入れると、泥沼にはまりそうな気がしてきました。
この問題が厄介なわけ
Shiro(2008/02/26 07:33:10 PST): 返事が遅くなりましたが、これはマクロの根本に 関わる非常に興味深い問題でして、なかなかまとめる時間が取れません。
とりあえず答えだけ言っておくと、解決法はたぶんふたつあって、 first class macroを認めるかhygienic macro(の拡張)を使うかということになります。 Scheme的な解決法は後者です。ただし現在のGaucheでは必要な手続きが 一般には見えるようになっていません。無理やりやるならこんな感じになります。
(let ((lambda. ((with-module gauche.internal global-id) 'lambda))) ((eval `(,lambda. (arg) arg) (interaction-environment)) 123))
あいにく、global-idはcompile.scmの内部手続きなので将来に渡って使える保証は ありません。将来的にはたぶんこう書けるようにすると思います。
(let ((lambda. (syntax lambda))) ((eval `(,lambda. (arg) arg) (interaction-environment)) 123))
((with-module gauche.internal global-id) 'lambda) で返される値は #<identifier gauche#lambda> ですが、これはgaucheモジュール中のlambdaを 示す識別子です。lambdaというシンボル自体は環境によってその意味を変えてしまいます (たとえば当該モジュール中でlambdaというグローバル変数が定義されてたり、 lambdaというローカル変数が定義されてたりする場合)。 しかし #<identifier gauche#lambda> はどこに持っていってもgaucheモジュール 中で定義が与えられているlambda、を示しているので、gaucheモジュール中のlambdaを 破壊的に変更しない限りは意味が変わりません。
なぜ#<syntax lambda>という「値」ではだめかというと、Schemeの意味論では 変数と変数の値との結びつきが実行時に生じるものであるのに対し、 構文キーワードと構文の意味との結びつきは実行に先立って判明していなければ ならないからです (他の多くの言語もそうです。ただ、そうでない言語もあります)。
(あとは時間が出来たら書くかもしれない)。
- 通りすがりです
(let ((lambda-entity lambda)) ((eval `(,lambda-entity (arg) arg) (interaction-environment)) 123)) => 123
これが動かない理由は2つ、最初はevalがletの束縛を参照できないため。つまり(let ((list2 list)) (eval '(list2 1 2) (interaction-environment)))
がエラーになるのと同じ理由。2番目は(unquote lambda-entity)を使っていること、結論から言いば、下記のように書けば123を得ることができます。(define lambda-entity lambda) ((eval '(lambda-entity (arg) arg) (interaction-environment)) 123)
ちなみにguileの振る舞いはあまり参考にしない方が良いです。かなり訛りが強いですから(笑
- Shiro(2008/02/26 22:23:13 PST): 外してます>通りすがりさん。
最初の例でevalはletの束縛を参照していません。quasiquoteの展開は
evalに渡される前に行われますから、evalが見るのは第一要素に#<syntax>が
入っているリストです。
そして、通りすがりさんの「結論」は今回の問題(syntactic binding の意味)について解決をもたらすものではありません。
- 通りすがりです(blogもwikiも持ってないのでこんな名前で失礼します)
これは申し訳ないことをしてしまいました
(let-syntax ((lambda-alias (syntax-rules () ((_ . x) (lambda . x))))) ((eval '(lambda-alias (arg) arg) (interaction-environment)) 123))
な気分で読んでしまい間違えてしまいました。すみません。 ところで、Gaucheでreplに'if'を打ち込んで#<syntax if>が帰るのは(syntax if)のabbreviation。(define if2 if)ができるのは(define-syntax if2 ...)のabbreviationになっているからと解釈していました。そう考えると他のシステムと同じように考えられるからです。この解釈で何か問題になるのか興味があります。Shiroさんの時間の許すときに書いていただけると嬉しいです。
Gaucheにおける#<syntax ...>の意味
Shiro(2008/02/26 23:20:55 PST): ああ、#<syntax ...> という表記が紛らわしいのかもしれません。
(syntax expr) が返すべきsyntactic objectとは、実装側から見れば式とその式を 解釈するコンパイル時環境とをセットにしたものです。特に (syntax <symbol>) は <identifier> を返し、これはそのシンボルがプログラム中でどのように束縛されているか (e.g. 標準Scheme環境のグローバル束縛を参照しているのか、ローカルに定義された 変数の束縛を参照しているのか、はたまたマクロ展開によって挿入されたのか、など) を保持しているオブジェクトです。
一方、Gaucheにおける#<syntax ...> というのはコンパイラがその構文を解釈する時に呼び出すハンドラのようなものです。 本来、それはコンパイル時にさえ存在していれば良く、実行時には意味を持たないもの のはずなのですが、Gaucheではコンパイルと実行がインターリーブするのと 簡素化のために、グローバル環境にコンパイル時束縛と実行時の値の束縛とをいっしょくたに して放り込んでいます。ifを評価して#<syntax if>が帰ってくるのはその 予期せぬ副作用であると思ってください。
Scheme:マクロ:CommonLispとの比較でちょっと出しましたが、Schemeの 意味論ではS式はまずマクロ展開のために情報を保持したsyntactic objectに変換され、 そこでマクロ展開が行われてからコンパイルされて実行可能なオブジェクトになります。 (ここでの「executable」はそのScheme処理系が内部的につかう何らかの実行形式という ことで、OSが実行可能なバイナリファイルとは限りません。実装によってはGaucheのように VMインストラクション列であったり、あるいは単なるツリー構造かもしれません。 また、ここでは"parse"と書きましたがこのフェーズにはっきりした名前は規格上は 与えられてないと思います。)
parse compile execute S式 --------> syntactic object --------> executable ---------> 値
で、構文キーワードやマクロなどはcompileの段階で意味が確定する必要があります。 これは、「コンパイル時環境」でsyntactic objectに対応する構文やマクロの定義を 引っ張ってくることで実現されます。例えばローカルにシャドウされていないifが出現 した場合、こんな感じで:
parse コンパイル時環境 if --------> #<identifier gauche#if> -----------------> #<syntax if>
コンパイラはそのifがGaucheで定義されているifの意味であることを知り、 #<syntax if>が保持しているハンドラを使ってコンパイルを行います。
一方、ローカルにシャドウされていないcarが出現した場合:
parse 実行時環境 car -------> #<identifier gauche#car> ----------------> #<subr car>
#<identifier gauche#car> にはコンパイル時に特に意味が与えられて いないのでコンパイラはスルーして、それを直接executableに埋め込みます。 そしてそのコードが走る時に、実行時環境からcarに対応する値が引っ張られて、 #<subr car>が得られます。
define-syntax, let-syntax, letrec-syntaxはコンパイル時環境を変更する操作で、 defineやletは実行時環境を変更する操作です。 従って概念的には、両者は全く別の世界に存在するものと考えた方がすっきりするでしょう。 Gaucheで(define if2 if)が可能なのはグローバル実行時環境とグローバルコンパイル時環境 が実装の都合で重なっていることの副作用で、ifが構文だからdefine-syntaxのabbreviation と解釈している、というわけではないです。
将来的に、コンパイル時環境の移し替えを実現するために (define-syntax if2 if) というのを許すかもしれませんが、 (define if2 if) は意味的におかしいのでいずれはフェードアウトすると思います。
- 通りすがりです。
ご丁寧な解説ありがとうございました。おかげさまですっきり理解することができました。
ところで、いろいろ考えてみて確かに実行時にマクロの定義が換わるのは特にネィティブなコンパイラの最適化を考えると不利なんだろうなと思いました。しかしインタラクティブな環境を考えればGaucheのマクロの扱いには大きな利点があると気がつきました。例として引数を印字するデバッグ用のlambdaの定義を考えてみます。Gaucheでは簡単にこう書けますが、
(define primitive-lambda lambda) (define-syntax lambda (syntax-rules () ((_ (f ...) x ...) (primitive-lambda (f ...) (format #t "trace-lambda: ~s ~%" (apply list f ... '())) (let () x ...))))) (define foo (lambda (x y z) (+ x y z))) (foo 1 2 3) => trace-lambda: (1 2 3)
これをr5rsで単純に書き直せばこんな感じでしょうか(define-syntax primitive-lambda (syntax-rules () ((_ . x) (lambda . x)))) (define-syntax lambda (syntax-rules () ((_ (f ...) x ...) (primitive-lambda (f ...) (format #t "trace-lambda: ~s ~%" (apply list f ... '())) (let () x ...))))) (define foo (lambda (x y z) (+ x y z))) => どの処理系でもマクロ展開が停止しない!
もしかしたらうまい方法があるのかもしれませんが、簡単には思いつきませんでした。 むむむ、、、これはなるほど難しい問題ですね・・・
- Shiro(2008/02/27 02:03:49 PST): R5RSだとトップレベルのdefine-syntaxで 既存の束縛を置き換えた場合の意味が規定されてないので、ポータブルに書くのは 不可能だと思います。R6RSならモジュールを分けることで可能です。