kiyokaのブログアーカイブ

Archive of old blog posts

set!の実装につまづく(2)

私がRubyで書いているLisp方言、 [Nendo]について。

先日の記事([kiyoka.2010_02_21]『set!の実装につまづく』)の続き。 shiroさんのコメントで間違いを御指摘いただいたのと、問題点がうまく記述できていなかったため、もううちど原点に戻って整理しよう。

[Nendo]の設計方針

クロージャのレキシカルスコープ等の管理はRuby処理系まかせにする

[Nendo]ではLispコードを等価なRubyコードにトランスレートすることで、クロージャのローカル変数(=レキシカル変数)のスコープ管理を全てRuby本体の機能にまかせることにする方針。自分で書くよりも、コード量が削減できるし、Ruby VMの最適化の恩恵にも浴することができるのではないかというのが狙い。 (クロージャのレキシカル変数がちゃんと動くようになったのはRuby 1.9.1からです。昔のRubyでは挙動が違うことに注意)

Rubyのインスタンス変数とローカル変数の両方を使う

Schemeのグローバル変数に相当するものをRubyのインスタンス変数 (Rubyでの表記は@var) に割当て、Schemeのローカル変数に相当するものを、Rubyのローカル変数 (Rubyでの表記はvar)に割りあてる方式でやることにした。 当初、Schemeのグローバル変数の機能もRubyのローカル変数だけで代用できるのではと思ったが、Rubyのローカル変数はRubyのコード実行時に割当てが決まるのではなく、Rubyコードのコンパイル時点で割当てわれてしまうため、コンパイル時に未定義の変数は永遠に未定義のままとなる。 これでは、トップレベル関数の相互呼出ができないとか、replでトップレベル関数の差し替えなどもできないという問題がある。 実験コード

  1  #!/usr/local/bin/ruby
     
     #
     # Rubyのローカル変数に割当てたクロージャは、pre-definedでないと呼出せない。
     #
     def test1
       proc1 = lambda {
         p "proc1(1)"
       }
       proc2 = lambda { # <--- entry point
         p "proc2(1)"
         proc1.call()
         p "proc2(2)"
 14      proc3.call()
         p "proc2(3)"
       }
       proc3 = lambda {
         p "proc3(1)"
       }
       
       proc2.call()
     end
     
     test1()

実行結果 14行目でproc3が未定義になる。(NameError例外)

"proc2(1)"
"proc1(1)"
"proc2(2)"
local_variable_test2.rb:14:in `block in test1': undefined local variable or method `proc3' for main:Object (NameError)
	from local_variable_test2.rb:21:in `call'
	from local_variable_test2.rb:21:in `test1'
	from local_variable_test2.rb:24:in `<main>'

補足として、test1()メソッドの入口でproc3 = nilとしておくと、上のエラーは回避できるが、Lispのrepl上では未来にユーザーがどのような変数を使うかは神のみぞ知るなので、ここでは使えない。

set!の実装方法

set!の代入対象の変数がローカル変数か、グローバル変数か、はたまた未定義かを生成後のRubyコードで動的に判断させる方法を最初に試したが、ダメだった。 例えば、(set! a 2) をRubyに変換したイメージを下記に示す。これではうまくいかない。

      begin
        if defined?(a) == "local-variable"
  3       _a = 2     # local variable
        elsif self.instance_variables.include?(:@_a)
  5       @_a = 2   # global variable
        else
          raise NameError
        end    
      rescue => __e
        raise __e
      end

前回の記事 ([kiyoka.2010_02_21]『set!の実装につまづく』) でも書いたように、Rubyのローカル変数の代入は非常に特殊で、if false then a = 2 end の様に例え実行されなくても、それ以降は ローカル変数 a が『宣言』されたことになってしまう。(前回の記事で『ブロック内のどこかに代入が記述されただけで(実行されなくても)、ローカル変数が宣言されたことになってしまう。』と書いたのは私の勘違いでした…すみません)

例えば、上の実装イメージでset!が連続で記述されたら1回目と2回目の set! の挙動が変わってしまうだろう。(たぶん、1回目はグローバル変数を更新し、2回目は新しく宣言したローカル変数を更新する挙動になるだろう)

(define a 100)
(set! a 2)
(set! a 3)

下記の実験コードで試してみよう。 実験コード

      #/usr/local/bin/ruby
      
      def local_variable_test
        func1 = lambda {||
          print "  ---- (1) ----\n" 
          begin
            print "a               : " ; p a
          rescue NameError
            puts "NameError(1)"
          end
          print "defined?(a)     : " ; p defined?(a)
          print "local_variables : " ; p local_variables
 13       if false
 14         a = 1     # assign
 15       end
      
          print "  ---- (2) ----\n" 
          begin
            print "a               : " ; p a
          rescue NameError
            puts "NameError(2)"
          end
          print "defined?(a)     : " ; p defined?(a)
          print "local_variables : " ; p local_variables
          if false
            a = 1     # assign
          end
        }
      
        func1.call()
      end
      
      local_variable_test()

実行結果

bash$ ruby local_variable_test.rb
  ---- (1) ----
a               : NameError(1)
defined?(a)     : nil
local_variables : *:a, :func1*
  ---- (2) ----
a               : nil
defined?(a)     : "local-variable"
local_variables : *:a, :func1*

func1の —- (1) —- と —- (2) —- 部分は同じコードの繰返しなのに、13,14,15行目の実行されない代入が登場するとそれ以降ローカル変数a はNameErrorにならない。 Rubyの仕様ではローカル変数が宣言されるとnilが代入される様だ。(今回の問題には関係ないけど、local_variablesメソッドの出力結果が defined?(a)の結果と対応していないのが変だが、それは気にしないでおこう。Rubyのバグかな?)

まとめ

このように、たとえ実行されない代入文でも宣言となる仕様では、set!に対応するコードをRubyで動的に切りかえる事はできない。 よって、前回の記事でも書いた様に、LispからRubyへの変換時にその時点での対応するRubyコードに切りかえて出力しないといけない。

今回のset!対応は生成Rubyコードを人間に見やすくインデントするリファクタリングをやった後にやろう。