5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rubyの for を「使うな」で済ませている人は、変数スコープで一回事故ってほしい

5
Posted at

はじめに

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らしくない」では説明がつかない、挙動の差です。

foreach は同じものではありません

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_objectinject を使う、あるいはループ前に変数を宣言しておく方が、ほぼ常に綺麗になります。
  • break の戻り値を気にしたくないとき: for は式全体が配列を返します。each も同じですが、break した際の挙動がわずかに違います。実用でこれが効いた経験は、私にはありません。

要するに、for でしか書けないコードはほぼ無く、for の方が綺麗になるコードもほぼ無い」 というのが実情だと思います。

結論: 「使うな」で覚えるのではなく、「違う」で覚えてほしいです

Rubyで for を使わない、という結論だけなら3秒で覚えられます。

でも本当に大事なのは、
for は古いからダメ」
ではなく、
foreach と同じ顔をして、スコープだけ違う」
ということです。

同じように見えるコードが、同じように壊れるとは限りません。

Rubyの怖さと面白さは、だいたいこういう小さなところにあります。

5
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?