はじめに
Rubyを書き始めると、だいたい3日目くらいに「for は使うな、each を使え」と教わります。
そして、ほとんどの人はその理由を 「Rubyらしくないから」 くらいで止めてしまっています。
止めている人に、ちょっと意地悪な質問をします。
x = "important"
[1, 2, 3].each do |x|
end
puts x # ?
for x in [1, 2, 3]
end
puts x # ?
上は "important"、下は 3 を返します。
これは「Rubyらしくない」では説明がつかない、挙動の差です。
for と each は同じものではありません
Ruby初学者向けの記事は、だいたい「for は内部で each を呼んでいる、だから each の方が直接的でRubyっぽい」で終わってしまいます。
半分は合っています。内部で each を呼んでいるのは本当です。ただ、肝心の差が語られていないことが多いです。
差はここにあります。
each はブロックを作ります。特に do |x| の x は ブロックパラメータ として扱われ、外側に同名のローカル変数があっても、それを上書きしません。
一方 for x in ... の x は、外側のローカル変数と同じスコープに置かれます。だからループを抜けても値が残りますし、外に同名の変数があれば踏み潰します。
ここで一点、誤解されやすいので補足しておきます。
「each のブロック内は外と完全に独立している」と言ってしまうと言い過ぎです。外側ですでに定義されているローカル変数は、each ブロックの中からでも普通に更新できます。
y = nil
[1, 2, 3].each do |x|
y = x * 2
end
puts y # => 6
これは動きます。y は外で定義済みなので、ブロック内からの代入が外の y に届きます。
一方で、こちらは少しややこしい挙動になります。
[1, 2, 3].each do |x|
y = x * 2
end
puts y # => nil
NameError にはなりません。nil が返ります。
ここがRubyのスコープの紛らわしいところで、y = という代入式がソースコード上に書かれている時点で、Rubyはパース時に y をローカル変数として認識します。ブロックの中であってもです。だから puts y は「未定義の変数を参照した」ことにはならず、「定義はされているが値が入っていない変数を参照した」扱いになり、nil が返ります。
ただし、ブロック内の y = x * 2 の代入結果が外の y に届いているわけではありません。届いていたら 6 になるはずですが、なりません。ブロック内で初めて代入された y は、ブロックローカルな別物として扱われているわけです。
つまり each のブロックスコープが効くのは、「ブロックパラメータ |x|」と「ブロック内で初めて定義された変数」 に対してです。外で定義済みの変数を触る分には、外と地続きで動きます。
for はこの区別自体を持っていません。for x in ... の x も、ループ内で初めて定義した変数も、全部メソッドのローカル変数として外に置かれます。
ここで一回、事故ってほしいです
実害が出るのは、こういうコードです。
x = "important"
[1, 2, 3].each do |x| # ブロックパラメータなので、外のxは無事
# ...
end
puts x # => "important"
for x in [1, 2, 3] # 外のxを上書きしにいく
# ...
end
puts x # => 3
each は外側の x を守ってくれます。for は容赦なく踏み潰します。
これを知らずに for を使うと、「変数名がたまたま被った」だけでバグります。しかもループが終わった"後"の挙動でバグるので、原因究明が遅れがちです。
「Rubyらしくない」ではなく、普通に危ないのです。
では for を使うべき場面はあるのか
「ある」という立場で書き始めて、書きながら何度か考え直しました。
正直な結論を書くと、「ある」とされている場面はあるものの、現代のRubyではほぼありません。
一応、挙げるとすれば、
-
ループ後に変数を使いたいとき: 上の例の挙動を"わざと"利用するパターンです。ただ、
each_with_objectやinjectを使う、あるいはループ前に変数を宣言しておく方が、ほぼ常に綺麗になります。 -
breakの戻り値を気にしたくないとき:forは式全体が配列を返します。eachも同じですが、breakした際の挙動がわずかに違います。実用でこれが効いた経験は、私にはありません。
要するに、「for でしか書けないコードはほぼ無く、for の方が綺麗になるコードもほぼ無い」 というのが実情だと思います。
結論: 「使うな」で覚えるのではなく、「違う」で覚えてほしいです
Rubyで for を使わない、という結論だけなら3秒で覚えられます。
でも本当に大事なのは、
「for は古いからダメ」
ではなく、
「for は each と同じ顔をして、スコープだけ違う」
ということです。
同じように見えるコードが、同じように壊れるとは限りません。
Rubyの怖さと面白さは、だいたいこういう小さなところにあります。