端的には以下のようなコードが問題になる。
(read (open-string-input-port "#!r6rs #t")) (call-with-string-output-port (lambda (out) (write '|\|| out))) ;; expect: "|\\||" ;; actual: "\x7c;"要するにreadが読み込んだ
#!
がVM全体に影響を与えるという問題である。リードテーブルはポート単位なのだが、#!
はファイル単位で管理されているのが大元の問題といえばそうなる。ではどうするか?とりあえず思いつく解決案としては、loadで読まれたのかread(もしくはそれに準じる手続き)で読まれたのかが分かればVMのフラグを立てる立てないのチェックが出来そうである。(そもそも、
#!
がリーダー以外に影響を与えるというのがよくないというのもあると言えばあるのだが、それを直すのはちと大変なのだ。)上記の解決案で問題になることがあるとすれば、ユーザーがreadとevalを用いて自作loadを作った場合だろう。そこまで考慮にいれるべきなのかという話でもあるのだが、Scheme精神としてはYesで個人的にはNoだったりする。ただ、既に見えている問題というのは大抵「後で自分が踏む可能性が高い問題」と言い換えることができるので、ここで手抜きして後で苦労するか、ここで苦労して後で楽をするかである。怠惰なプログラマを目指す僕としては「未来の自分が楽をするために努力する」べきだと思うのでもう少し抜本的な解決策を模索するべきだろう・・・
4 comments:
file1.scmで
#!r6rs
(define (foo x) (write x))
としてあって、file2.scmで(仮にr7rsモードを#!r7rsで指示するとして)
#!r7rs
(foo '|\||)
とした場合の出力はどうなるのでしょう。
伝統的なLispだと、writeの「動作モード」は動的環境と考えて、fooの定義がどこにあろうが呼び出し時点でのモードが有効、とする感覚だと思いますが、「ファイル単位のフラグ」ということはfooの定義時のモードがキープされる?
これは書き方が悪かったですね。この場合だとR7RSモードで出力されるので、伝統的なLispの挙動という形になります。ファイル単位=load単位という感じです。定義された手続き、その他はVMの動作モードを一切保存しないので、呼び出し時に最終的にセットされたモードが有効になります。
ちょっとトリッキーなのが以下のようなので、
#!r6rs
(begin (write '\x7c;) #!r7rs #t)
これの出力は最後に読まれるのが#!r7rsなのでR7RS仕様の出力(|\||)になります。 (見た目にも個人的にも混乱を招くなぁと思うのですが、#!でモード変えてるうちはどうしようもないという・・・)
なるほど、「ロード単位」というのは、(1)loadの実行時に概念的には新たな動的環境が作られて (2)#!はその動的環境を破壊的に変更し (3)load終了時にその動的環境を破棄する、っていうモデルだと推察します。(1でコピーを保存しといて3で戻す、でも良いんですがモデルとしては同じことですね)
コードの読まれる順番と実行される順番は(トップレベルフォーム以外は)一致しないのがむしろ普通なので、#!が実行に影響を与えるとした時点で綺麗な解決はないと思うんですが、ヒューリスティックに解決するなら、loadが呼ぶトップレベルのreadだけ何らかの方法で特別扱いするという、本文中の「とりあえずの解決策」しかないと思います。
readの下請けとして言語モードのコンテキストを受け渡して行くread/contextみたいな関数を作り、loadはモナドとしてコンテキストを流すように書くということにして、ユーザが自分でloadを作りたければread/contextを使ってね、としておけば良いんじゃないかと。
(ついでに#!トークンがトップレベルに出てきたかどうかを区別できるようにしておけば、上の「トリッキーなコード」の意味づけもユーザが選べると思います。)
まさにそのモデルです。これ以外の呼び出し先の変更が呼び出し元に影響を与えるのを避ける方法を思いつかなかったので。
やっぱりそれしかないですか。read/contextはいいですね、コンテキストをどう見せるかというところがちょっと悩ましいですが、(現状かなりコードが汚いので)すっきり書けそうです。
Post a Comment