set!の実装につまづく
私がRubyで書いているLisp方言、 [Nendo]について。
ちょっと作業する時間があったので、set!をSchemeの仕様に近づける修正にトライした。
つまづいたので備忘録として記事にしておく。
方針
方針としては、Schemeのグローバル変数に相当するものをRubyのインスタンス変数 (Rubyでの表記は@var) に割当て、Schemeのローカル変数に相当するものを、Rubyのローカル変数 (Rubyでの表記はvar)に割りあてる方式でやることにした。
LispからRubyへのトランスレートにおいて次の様なコードを吐く様にした。 (以下は原理を簡単にする為に、実際に[Nendo]処理系が出力するコードを簡略化してある。一応動く)
- トランスレート前のLispコード
(define a 100) (let ((a 1)) (set! a 2))
- トランスレート後のRubyコード
#/usr/local/bin/ruby @_a = 100 lambda {|a| begin 6 if defined?(a) == "local-variable" puts "(1)" 8 a = 2 # local variable elsif self.instance_variables.include?(:@_a) puts "(2)" 11 @_a = 2 # global variable else raise NameError end rescue => __e raise __e end 18 p local_variables }.call(1)
実行結果
ruby localvar_test.rb (1) *:a, :__e*
どこがダメか
上記のコードでは11行目が実行されるかと思いきや8行目のほうが実行される。 ローカル変数 a が8行目で宣言されてしまうので、6行目の判定は定義済となってしまうのだ。 Rubyのローカル変数の代入は非常に特殊で、ブロック内のどこかに代入が記述されただけで(実行されなくても)、ローカル変数が宣言されたことになってしまう。
変数と定数 - Rubyリファレンスマニュアルの引用 宣言は、例え実行されなくても宣言とみなされます。 v = 1 if false # 代入は行われないが宣言は有効 p defined?(v) # => “local-variable” p v # => nil
手ぬきの代償
[Nendo]ではLispコードを等価なRubyコードにトランスレートすることで、クロージャが持つローカル変数のスコープ管理などを全てRuby本体の機能にまかせるという前提だったのだが、上記のやりかたでは十分でないということになる。 Schemeのset!と等価なローカル変数の代入を実装するためには、そのローカル変数が定義済かどうかの判定を生成されたRubyコード自身にやらせるのでは遅すぎるということだ。
対策
Rubyへトランスレートする前のLispコード(S式)の段階でレキシカルスコープの解析を行い、それぞれのset!が出現した場所でそのローカル変数が定義済かどうかを判定して、各set!に対応するコードを動的に切りかえる。 多分これで実現できるだろう。
感想
Rubyのローカル変数の代入と宣言の仕様は、なかなか微妙な仕様だと思う。 Schemeが define(定義) と set!(代入)の役割をはっきり分離しているのに対して、Rubyはそこを分かちがたく統合してしまっている。 そこには、Rubyなりの設計方針でそうなっているのだとは思うが、Schemeと比べるとスッキリしないなあ。 Rubyの『驚き最小の法則』というのを昔聞いたことがあるが、今回のは個人的にちょっと驚いた。 というか、せめて宣言と代入を分離する手段も用意しておいてほしかったぞ。
参考: yugui wiki - 『初めてのRuby』余った切れ端 yuguiさんの見解が書かれているページを見つけた。
初期値 (6章余り) Column: 初期値 (略) Ruby文法は妥協と折衷、損益判断により構成されています。Ruby文法がなぜ Aであるかを調べると、いつも「Bにするだけの価値があるか」という点が浮 かび上がってきます。変数は基本的に代入による初期化を必須にする方針の 一方で、インスタンス変数とグローバル変数についてはアクセス可能なコー ド範囲が広すぎて初期化を強制するには弊害が大きすぎたのだと考えられま す。
コメント by shiro:
rubyの仕様はまだよく理解できてないですが、このコードに限ればdefined?(a)がlocal-variableになるのはlambda式の仮引数が|a|になってるからじゃないでしょうか。試しに lambda {|z| …} に変えてみたら11行目の方が実行されました。
仮引数が|a|なのは (let ((a 1)) …) だから、ということなら、8行目の方が実行されるのが正しいですよね。
ただまあ、Schemeコードを処理する時に変数がローカルかグローバルかを判定するのはごく簡単なので (サブフォームに再帰してゆく時にローカルの変数リストを渡して行くだけ)、そうしてしまった方がうんと楽だとは思います。あと、その時点でset!に対応するコードを切り替えるのは、「動的」とは言わないと思います。コードそのものの実行時じゃないから。
コメント by kiyoka:
shiroさん、コメントありがとうございます。
ご指摘の通り、上記の実験に致命的な間違いがありました。
この記事での問題点の記述も微妙にずれております。
再度、問題点を整理して次の記事にしてみます。
ただ、解決方法はshiroさんのコメントのように、サブフォームにローカル変数のリストを渡す方法でいけると思います。
また、Rubyコード生成時にset!のコードに対応するRubyコードに切り替えるのは、どちらかというと「動的」でなくて「静的」ですかね。
コメント by shiro:
rubyの仕様はまだよく理解できてないですが、このコードに限ればdefined?(a)がlocal-variableになるのはlambda式の仮引数が|a|になってるからじゃないでしょうか。試しに lambda {|z| …} に変えてみたら11行目の方が実行されました。
仮引数が|a|なのは (let ((a 1)) …) だから、ということなら、8行目の方が実行されるのが正しいですよね。
ただまあ、Schemeコードを処理する時に変数がローカルかグローバルかを判定するのはごく簡単なので (サブフォームに再帰してゆく時にローカルの変数リストを渡して行くだけ)、そうしてしまった方がうんと楽だとは思います。あと、その時点でset!に対応するコードを切り替えるのは、「動的」とは言わないと思います。コードそのものの実行時じゃないから。