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のエニュメレータ内での破壊行為はもう止めようよ!