[Ruby][DONE] ローカル変数が生成されるタイミング

疑問: なぜ "ruby -e 'p(v=v)'" が NameError にならないのか?

Rubyのレキシカルスコープのすごさを同僚に説明している中で気づいた。

% ruby -e 'p v'
-e:1: undefined local variable or method `v' for main:Object (NameError)
% ruby -e 'p (v=v)'
nil

なぜ、未定義変数を自分自身に代入するときには、NameErrorにならないのだろう。

答えは、Rubyのパーサが「代入文を見つけた時点で変数が定義される」仕様になっているため。

以下、詳細。

定数の場合は NameError になる

% ruby -e 'p(FOO=FOO)'
-e:1: uninitialized constant FOO (NameError)

変数もこの挙動を期待してたのだが。

Rubyの変数仕様

以下の記述を見つけた。

宣言は、例え実行されなくても宣言とみなされます。

v = 1 if false # 代入は行われないが宣言は有効
p defined?(v)  # => "local-variable"
p v            # => nil
http://doc.ruby-lang.org/ja/1.9.3/doc/spec=2fvariables.html

これは、「パーサが代入文を見つけたときに、変数定義をしてしまう」ということだろうか。

RHG に書いてあった

RHGRuby Hacking Guide, もしくは『Rubyソースコード完全解説』)に答えが書いてあった。
(2ヶ月前に読んだばかりなのに、思い出せなかった…)

「12.4.1 ローカル変数の定義」
(中略)
ところで、定義されるのは代入が「現れたとき」なので、実際には代入が行われなくても定義されるということになる。定義された変数の初期値はnilだ。

if false
  lvar = "この代入は決して実行されない"
end
p lvar   # nil と表示される

またさらに、定義されるのは代入が「現れた」「とき」なので記号列上で参照の前になくてはいけない。たとえば次のような場合は定義されていない。

p lvar      # 定義されていない!
lvar = nil  # ここで現れているのだが…

「記号列上で」というところに注意してほしい。評価順とはまるで関係がない。たとえば次のようなコードなら当然条件式を先に評価するが、記号列順だとpの時点ではlvarへの代入は現れていない。したがって NameError になる。

p(lvar) if lvar = true

なるほど、つまり、プログラム中で代入文が「現れた」タイミングで変数は定義され、そのときの値はnilになる。
その後、実際の代入処理が「評価」されたタイミングで、本当の値が代入される、ということか。

動作を確認

上記を確かめてみる。

if false; vv=true; end; p vv  # => nil
p (vv) if vv=true             # => NameError
vv=vv; p vv                   # => nil

つまり、最後の例では、

  1. 左辺の "vv=" をパーサが発見 → ローカル変数 vv を定義し、nil を代入
  2. "=" 演算子を右から解釈する
  3. vv の値をとりだす → nil
  4. vv に nil を代入

という順番で、 "vv=vv" の評価結果が nil になるのか。

Rubyソースを見る

"vv=vv" を評価するときは、 parse.y の以下のルールが使われてるようだ。

lhs : user_variable
    {
	$$ = assignable($1, 0);
    /*%%%*/
	if (!$$) $$ = NEW_BEGIN(0);
    /*%
	$$ = dispatch1(var_field, $$);
    %*/
    }

arg : lhs '=' arg
    {
    /*%%%*/
	value_expr($3);
	$$ = node_assign($1, $3);
    /*%
	$$ = dispatch2(assign, $1, $3);
    %*/
    }

C の中では、以下の呼び出し連鎖:

assignable()
  local_var_gen()
    vtable_add()

つまり、パーサが変数代入を見つけたタイミング(lhsへのREDUCE時)に変数が設定(値はnil)されて、その後に右辺を評価する、という動作。
今までの結果とも合致するので、この考え方でよいみたい。