1.9 では Enumerator が組み込みになり、大きく拡張されています。ついでにブロックの省略に対する考え方にも影響があります。結構重大な変更のわりに、この話はあまり議論や周知がされていないような気がしたので、現状の Enumerator について、その機能と問題点をまとめてみました。
Enumerator の機能
まず、each や map など、イテレータっぽいメソッドをブロックなしで呼び出すと Enumerator が得られます。
p [1,2,3].each #=> #<Enumerable::Enumerator:0xb7d38260> p [1,2,3].map #=> #<Enumerable::Enumerator:0xb7d38210>
Object#to_enum または enum_for を使って、指定したメソッドによる Enumerator を明示的に作ることもできます。
p [1,2,3].to_enum(:each) #=> #<Enumerable::Enumerator:0xb7d51350>
Enumerator には (僕が知る限り) 3 つの機能があるようです。
外部イテレータとして使える
最も大きな機能です。大きな機能だけど、どのくらい使えるのかはよくわかりません。Python 派には受けがいい?
e = [1,2,3].each # 順に取り出す p e.next #=> 1 p e.next #=> 2 # 巻き戻し e.rewind # 順に取り出す p e.next #=> 1 p e.next #=> 2 p e.next #=> 3 # 全部取り出した後、次を取り出そうとしたら例外 p e.next #=> iteration reached at end (StopIteration)
これにあわせて、Kernel#loop のブロックの中で例外 StopIteration が発生したら loop を終了することになっています。
p loop { raise StopIteration } #=> nil e = [1,2,3].each loop { p e.next } #=> 1, 2, 3
Enumerable#with_index が使える
最も多くの人が待ち望んでいた機能です。each には each_with_index がありましたが、これが each 以外のイテレータでも使えるようになります。
p ["a","b","c"].map.with_index {|v, i| v + i.to_s } #=> ["a0", "b1", "c2"] p [2,1,0].find.with_index {|v, i| v == i } #=> 1
Enumerable#zip の引数に渡せる
最も地味な機能です。zip の引数に Enumerable を渡すことができます。
e1 = [4,5,6].each e2 = [7,8,9].each [1,2,3].zip(e1, e2) do |a, b, c| p [a, b, c] #=> [1, 2, 3], [4, 5, 6], [7, 8, 9] end
Enumerable#cycle と合わせるといい感じ? 参考: これを使って Fizzbuzz を書いた例。
Enumerator の問題点
「ブロック省略 → Enumerator を返す」の convention が従来と非互換
ブロックの有無は block_given? によってメソッド側で判断できるので、ブロックの有無によってメソッドの挙動を変えることができました。File.open や String#gsub が典型例です。
# ファイルハンドラを明示的に close する fh = File.open("foo"); puts fh.read; fh.close # ファイルハンドラを暗示的に close する File.open("foo") {|fh| puts fh.read } # b か c か d を * に置き換える "abcde".gsub(/[bcd]/, "*") #=> "a***e" # b か c か d をその大文字に置き換える "abcde".gsub(/[bcd]/) {|s| s.upcase } #=> "aBCDe"
つまり、ブロックを省略したときの挙動はそのメソッドの設計者が判断していました。ですが、「イテレータっぽいメソッドはブロック省略時に Enumerator を返す」という作法が発生することで、この決定権が制限されます。また、イテレータっぽいメソッドを自分で定義するとき、この作法に従うためのおまじないを手動で入れる必要があるのも面倒です。
module Enumerable # 奇数番目の値だけ列挙する def leapfrog # おまじない return to_enum(:leapfrog) unless block_given? f = false each {|x| yield x if f = !f } end end e = [1,2,3,4,5].leapfrog; loop { p e.next } #=> 1, 3, 5
「ブロック省略 → Enumerator を返す」とすべきメソッドの基準がわからない
Enumerable#grep 、count 、all? 、any? 、take_while などは、僕の感覚では十分イテレータっぽいメソッドなんですが、「ブロック省略 → Enumerator を返す」となっていません。つまり [1,2,3].count.with_index {|v, i| ... } とはできません。(追記: count と take_while は対応されました。)これらのメソッドがもともとブロックの省略に意味を与えていたためでしょうか。
p ["a","b","c","b","a"].grep("b") #=> ["b", "b"] p ["a","b","c","b","a"].grep("b") {|x| x + "." } #=> ["b.", "b."] p ["a","b","c","b","a"].count("b") #=> 2 p ["a","b","c","b","a"].count {|x| x != "b" } #=> 3 p [true, true, true].all? #=> true p [true, false, true].all? #=> false
でも Enumerable#map や zip もブロック省略に意味を与えていたのですが、なぜかこれらは仕様変更され「ブロック省略 → Enumerator を返す」となっています。よくわかりません。