Rubyノート - 変数のスコープとブロック、の落とし穴

プログラミングRuby 第2版 言語編』の p88 あたりで説明されている、

ブロックの実行時、ブロック内変数と同じ名前のローカル変数が既に存在していた場合は、その既存のローカル変数がブロック内でも使われます。

にまつわる問題に実際に出くわしたので、自戒の意味も込めて問題となったコードを貼り付けておく。(ちなみに、1.9 ではブロックパラメータのスコープ規則が変更になったのでこの問題は起きない。末尾の方を参照)

Ruby のブロックはクロージャだから、ブロック外のローカル変数を参照できるのは当たり前で、自分もそのことを積極的に利用して普段からコードを書いているくせに、ついうっかりこういう落とし穴にハマってしまう。参ったなあ。

以下が問題のコード↓

def self.validator(key, values, table = {})
  values.each {|val| table[val] = val }
  table.keys.each {|key| table[key.to_s] = table[key] }
  _validation_data[key] = [values, table]
  _validation_data[key].deep_freeze
end

盛大にやっちまってるのが、key
table.keys.each でブロックパラメータに key を使ったのが、メソッドの仮引数 key と正面からガチンコで衝突してしまった。

この結果、イテレーション後の key が意図したものとは異なる値になってしまっており、当然のごとく実行時にエラーを出してしまった。

ううむ。ブロックパラメータが実はブロックローカルじゃないってのが落とし穴になったようだ。そうでないローカル変数の場合は結構気を配っていたのだが。*1

ブロックパラメータはブロックローカルなのが自然じゃね? と思って調べてみたら 1.9 ではそのように変更された模様。

1.9 の動きは全然フォローしてなかったけど、改善すべきものは着実に改善されていってる印象。キーワード引数もよさげ。

とはいえ、当面は 1.8系を使い続けることになるので、今回のような問題には気を付けないといけないなー

*1:言い訳乙。しかもかなり苦しいw