Enumerator#with_indexの落とし穴
[ruby-list:45744] 無限ループの回数の取得にてKernel#loopによる無限ループでループの回数を知りたいという質問があった。
で、俺はイテレータをwith_index化する魔法のメソッドであるEnumerator#with_indexを使えばいいじゃんと思って試してみたらエラー。ありゃ、Kernel#loopってブロック必須なのか。ブロックなしでEnumerator返すんだと思ってたよ。
loop.with_index do |i| end # ~> -:1:in `loop': no block given (LocalJumpError) # ~> from -:1
[ruby-list:45747] Re: 無限ループの回数の取得でEnumerator返せよと提案したし、まつもとさんも賛成していた。
しかし、これは単純な話ではなくて、なかださんから[ruby-list:45751] Re: 無限ループの回数の取得にてEnumerator#with_indexの仕様が「yieldされたものの最後にindexを追加するというものではなく、yieldされたものとindexの二つをyieldする」というものだと知らされた。ということは、ブロックを取らないイテレータにwith_indexを渡すと、nilが入り込むってことだよな。
じゃあEnumeratorを返すKernel#loopを作成して試してみようっと。本家Kernel#loopはStopIterationとかをサポートしているのでlesser_loopというメソッド名で。
ちなみに「return to_enum(メソッド名) unless block」はブロック無しイテレータでEnumeratorを返すおまじないだ。
def lesser_loop(&block) return to_enum(:lesser_loop) unless block block.call while true end i = 0 lesser_loop do i # => 0, 1, 2, 3, 4, 5 break if i >= 5 i += 1 end
このKernel#lesser_loopでwith_indexをつけてみると、
lesser_loop.with_index do |i| # !> shadowing outer local variable - i i # => nil break if i >= 5 end # ~> -:20:in `block in <main>': undefined method `>=' for nil:NilClass (NoMethodError) # ~> from -:4:in `call' # ~> from -:4:in `lesser_loop' # ~> from -:18:in `with_index' # ~> from -:18:in `<main>'
ありゃ、エラーなるわ。
lesser_loop.with_index do |_,i| # !> shadowing outer local variable - i i # => 0, 1, 2, 3, 4, 5 break if i >= 5 end
たしかにnilが乱入してるよ。これはキモい><
Enumerable#mapやEnumerable#find等ブロックパラメータを取るメソッドと併用するのが普通だから気付かんかった。落とし穴だね。
ブロックパラメータを取らないイテレータの場合は特別にindexのみを取るように仕様変更したら、どうなんだろう…ってブロックパラメータを取らないメソッドにwith_indexを適用している人いる??
おいおい、bladeのタイトルがはてなじゃ化けるぞ。さっさと直してくれよぉ。