Ruby のメソッド呼び出しと変数参照について注意すること

最近、ハマっている人が何人書いたのでちょっとまとめてみます。

Ruby のメソッド呼び出し

Ruby ではメソッドを呼び出す場合に他の言語と比較して『() を省略してメソッドを呼び出す事』ができます。

def hoge(a = nil)
  "#hoge(#{a})"
end

# 括弧がなくてもメソッドを呼び出せる
p hoge
# => "#hoge()"

# メソッドっぽい呼び出しでもメソッドを参照できる
p hoge()      # => "#hoge()"
p hoge 42     # => "#hoge(42)"
p self.hoge   # => "#hoge()"

まあこれはそのとおりですね。

メソッド名と同名の変数名が定義されていたらどうなるの

問題は『同名のメソッドと変数』が混載している場合です。
この場合は『変数が参照できれば変数』を参照し、『そうでなければ』メソッドを参照します。

def hoge(a = nil)
  "#hoge(#{a})"
end

# この時点では変数が定義されていないのでメソッドを優先して呼び出す
p hoge   # => #hoge()

# 変数を定義する
hoge = 42

# 変数を定義したあとでは変数を優先して定義する
p hoge   # => 42

# メソッドっぽい呼び出しではメソッドを呼び出す
p hoge()      # => "#hoge()"
p hoge 42     # => "#hoge(42)"
p self.hoge   # => "#hoge()"

この時に注意するのは『代入式よりも前であればメソッド』を参照し、『代入式よりもあとであれば変数』を参照します。

変数が定義されるタイミングは?

例えば次のように『実際に変数を定義している処理が呼ばれないケース』が Ruby では存在します。

def hoge(a = nil)
  "#hoge(#{a})"
end

if false
  hoge = 42
end

# これはメソッド参照?
p hoge

これはインタプリタ言語的には『変数が定義されていないのでメソッドが呼び出される』ことを期待する方もおられるかもしれません。
しかし、実際には p hoge では『変数 hoge 』を参照します。
これは Rubyソースコードを実行する仕組みに秘密があります。
Ruby ではソースコードを実行する前にまず『全 Rubyソースコードをパースしてから』Ruby のコードを実行します。
なので、上のコードのように『実行時にそのコードが呼び出されるかどうか』というのは関係なく全ソースコードがパースされるので『代入式』が定義された時点で hoge という変数が暗黙的に定義されたことになります。
なので、実際に if 文の中身が呼ばれるかどうか関係なく『代入式』が定義された時点で『if 文の外』でも『変数 hoge』が参照されるようになります。

# 実際の実行結果
def hoge(a = nil)
  "#hoge(#{a})"
end

if false
  hoge = 42
end

# ここでは変数を参照する
# 変数のデフォルト値は nil なので nil を返す
p hoge
# nil

後置if + 変数定義

Ruby がどのようにソースコードをパースするのかを理解するのはとてもむずかしいです。
例えば次のように『後置if で hoge を参照しつつ hoge 変数を定義する』場合どうなるでしょうか。

hoge = 42 if hoge.nil?
p hoge

これは if hoge.nil? が変数定義よりも前に処理されるので実際には hoge = 42 という処理は呼び出されない、と考える人も多いと思います。
しかし、実際には hoge = 42 if hoge.nil? というコードは『これ全体で1つの処理』としてパースされます。
なので、 if hoge.nil? が呼び出された時点ですでに hoge = 42 というコードはパース済みになっており『変数 hoge は定義されている』という処理になります。
なので、先程のコードの実行結果は、

hoge = 42 if hoge.nil?
p hoge
# => 42

と、いう風に『 hoge = 42 』が処理された状態になります。
逆に後置 if でない場合は結果が異なるので注意する必要があります。

# これはエラーになる
# error: undefined local variable or method `hoge' for main:Object (NameError)
if hoge.nil?
  hoge = 42
end

これは if hoge.nil? が変数よりも前にパースされて『変数が定義されていない状態』で if 文の条件式が実行されるためです。

eval("hoge") するとどうなる

Ruby では eval というメソッドが存在します。
これは『実行時』に Rubyソースコードを実行するメソッドです。

def hoge(a = nil)
  "#hoge(#{a})"
end

hoge = 42
# eval に渡した文字列を Ruby のコードとして実行する
p eval("hoge + hoge")
# => 84

上のコードを実行すると代入式よりもあとで `"hoge + hoge" を実行しているので『変数』を参照します。
では、次のようなコードを実行するとどうなるでしょうか。

def hoge(a = nil)
  "#hoge(#{a})"
end

# 代入式よりも前に hoge を実行する
p eval("hoge")

hoge = 42

先程から説明している流れからいうと『変数よりも前に "hoge" を実行している』のでこれは『メソッド呼び出し』になることを期待します。
しかし、実際に実行すると以下のような結果になります。

def hoge(a = nil)
  "#hoge(#{a})"
end

# メソッド呼び出しではなくて変数を参照する
p eval("hoge")
# => nil

hoge = 42

なぜ、このような結果になるのかというと『 Rubyソースコードするタイミング』と『 eval を実行するタイミング』た異なる為です。
eval を実行するタイミングはあくまでも『Ruby の実行時』になります。
この『実行時』というのは『Rubyソースコードがパースされたあと』になります。
つまり『 eval を実行するタイミング』ではすでに『Rubyソースコードがパースされたあと』になっているため hoge という式は『変数を優先して』参照してしまうのです。
なので eval から変数やメソッドを参照する場合、『代入式の定義位置』に関係なく『変数』を優先して呼び出されるのです。
これは binding.irb を使用したときにも影響し、例えば次のように『代入式よりも前』で binding.irb を呼び出した時に問題になります。

def hoge(a = nil)
  "#hoge(#{a})"
end

# デバッグ等で binding.irb で実行時に irb を起動する
# この irb のコンソール上で hoge を参照すると『変数』を参照する
binding.irb

hoge = 42

binding.irb では入力した Ruby のコードを eval を用いて実行します。
なので、先程の例のように『変数』を参照して実行されることになります。
普段の Ruby のコードでは eval を使うととはめったにないと思うんですがこのように binding.irb などを使用すると間接的に eval を使うことになるので注意する必要があります。

まとめ

  1. Ruby では『 hoge 』という式がメソッド呼び出しなのか変数参照なのか曖昧である
  2. 変数が存在している場合は変数を優先し、そうでない場合はメソッド呼び出しを優先する
    • 代入式より前はメソッド呼び出し、それよりあとは変数参照にあんる
  3. ただし、動的に変数を参照する場合は変数を優先するので注意する
    • evalbinding.irb を使う場合は注意する
  4. 基本的にはメソッド名と同じ名前の変数名は避けるべきではある
    • 避けるべきではあるが実際に『どういうメソッド』が定義されているのか不透明なのでむずかしい 
  5. Ruby ではそれが『変数』なのか『メソッド』なのかを意識でコードを書くことが重要

と、言う感じで Ruby の変数についてまとめてみました。
実際に binding.irb でメソッドを呼び出した場合に nil が返ってくる事があり、よくよくコードを見てみたら binding.irb よりもあとで同名の変数名が定義されている事がありました。
このように Ruby ではハマりポイントがあるので注意しましょう。