Rubyのエニュメレータ内での破壊行為は止めてください!

RubyのArrayにはrotate!という便利なメソッドがあるよ。このメソッドは文字通り配列の要素をローテートするんだ。

 a = [1,2,3]
 a.rotate! # => [2, 3, 1]
 a.rotate! # => [3, 1, 2]
 a # => [3, 1, 2]

メソッド名の最後に!(ビックリマーク)があるから、これは元のオブジェクト自身を変えるよ。


昨日僕はこのrotate!メソッドにおけるローテートの過程を取りたいと思ったんだよ。で、次のようなコードを書いてみたんだ。

 a = [1,2,3]
 3.times.map { a.rotate! }


そうしたら期待したものとは違う、次のような結果が返ってきたんだ。

 # => [[1, 2, 3], [1, 2, 3], [1, 2, 3]]


あれ?mapがいけないのかな..

q = []
3.times { q << a.rotate! }
q # => [[1, 2, 3], [1, 2, 3], [1, 2, 3]]


ローテートしてないのかと思って、ブロック内でpしてみたらちゃんとしてるんだよ。

a = [1,2,3]
3.times.map { p a.rotate! } # => [[1, 2, 3], [1, 2, 3], [1, 2, 3]]

# >> [2, 3, 1]
# >> [3, 1, 2]
# >> [1, 2, 3]

なんか変だな..


で、少し考えたら理由がわかったんだ。Array#rotate!はselfを返すんだったよ。

a = [1,2,3]
a.object_id # => 2151892940
3.times.map { a.rotate!.object_id } # => [2151892940, 2151892940, 2151892940]


つまりmapの返り値はa.rotate!のスナップショットの配列を返すんじゃなくて、元オブジェクトの参照の配列を返すんだよ。で、mapの返り値はすべての要素に対するイテレートが終わってから返されるから(当然だよね)、その時点つまり最後のa.rotate!の後における元オブジェクトの状態がすべての配列の要素として返されることになるんだ。


つまりこれは次のコードと同じようなことなんだよ。

a = [1,2,3]
b = a.rotate!
c = a.rotate!
d = a.rotate!
[b, c, d] # => [[1, 2, 3], [1, 2, 3], [1, 2, 3]]


だからスナップショットつまり途中経過がほしい場合は、さっきみたいにpしたり、to_sしたりdupしたりする必要があるんだね。

a = [1,2,3]
3.times.map { a.rotate!.to_s } # => ["[2, 3, 1]", "[3, 1, 2]", "[1, 2, 3]"]

a = [1,2,3]
3.times.map { a.rotate!.dup } # => [[2, 3, 1], [3, 1, 2], [1, 2, 3]]


同じことはほかのRubyの破壊的メソッドでも起きるよ。

s = "hello, world!"
s.size.times.map { p s.chop! } # => ["", "", "", "", "", "", "", "", "", "", "", "", ""]
# >> "hello, world"
# >> "hello, worl"
# >> "hello, wor"
# >> "hello, wo"
# >> "hello, w"
# >> "hello, "
# >> "hello,"
# >> "hello"
# >> "hell"
# >> "hel"
# >> "he"
# >> "h"
# >> ""


うっかりしてるとまたミスしそうだよ。分かってる人には当たり前のことなんだろうけど、僕はちょっと嵌っちゃったから書いてみたよ :)


だからビルのエレベーター内での危険行為はもう止めようよ!
だからRubyのエニュメレータ内での破壊行為はもう止めようよ!