Enumerator-外部イテレータ
RubyでもJavaのような外部イテレータを作成することができる。配列、ハッシュなどのコレクションに対してto_enumメソッドを呼び出すことでできる。
a = [1, 3, "cat"] h = { dog: "canine", fox: "lupine" } enum_a = a.to_enum enum_h = h.to_enum enum_a.next #=> 1 enum_h.next #=> [:dog, "canine"] enum_a.next #=> 3 enum_h.next #=> [:fox, "lupine"]
内部イテレータはブロックなしで呼び出すと、Enumratorオブジェクトを返す。
a = [1, 3, "cat"] enum_a = a.each enum_a.next #=> 1 enum_a.next #=> 3
Rubyにはブロックを繰り返し呼び出すだけのloopメソッドが用意されている。ブロック内のコードは何らかの条件が満たされるとループから抜ける。loopはEnumeratorと組み合わせて使用するとさらに便利です。loop内でenumeratorオブジェクトの値がなくなると、loopが正規の手順で終了する。
short_enum = [1, 2, 3].to_enum long_enum = ['a'..'z').to_enum loop do puts #{short_enum.next} - #{long_enum.next}" end
出力結果:
1 - a 2 - b 3 - c
文字列内の各文字を繰り返し処理する場合にはどうするか?
stringクラスのeach_charメソッドはブロックを与えずに呼び出すとenumeratorオブジェクトを返す。このenumeratorオブジェクトに対してeach_with_indexメソッドを呼び出せばよい。
result = [] "cat".each_char.each_with_index {|item, index| result << [item, index] } result #=> [["c", 0], ["a", 1], ["t", 2]]
Enumeratorオブジェクトを明示的に作成することもできる。
enum = "cat".enum_for(:each_char) enum.to_a # =>["c", "a", "t"]
既存のコレクションからenumraotrオブジェクトを作成するだけでなく、作成時にブロックを与えることで明示的にenumeratorを作成することもできる。ブロック内のコードはプログラムが次の列挙値を読みとろうとしたときに使用される。このブロックは一気に実行されるわけではなく、プログラムの残りの部分と平行して実行される。つまり、ブロックは先頭から実行が開始され、コードに値を供給(yield)すると停止する。コード側で次の値が必要になると、yieldの直後の文からブロックの実行が再開される。これを使うと無限列を生成するenumratorを書くことができる。
triangular_numbers = Enumerator.new do |yielder| number = 0 count = 1 loop do number += count count += 1 yielder.yield number end end 5.times { puts triangular_numbers.next }
出力結果
1 3 6 10 15
enumerableのメソッドをenumeratorオブジェクトに対して呼び出すこともできる
p triangular_numbers.first(5)
enumeratorの持つcountやselectのようなメソッドは、結果を得るために全ての要素を読み取ろうとする。そのため、無限列に対して動作するselectメソッドが欲しければ自分で用意する必要がある。
triangular_numbers = Enumerator.new do |yielder| number = 0 count = 1 loop do number += count count += 1 yielder.yield number end end def infinite_select(enum, &block) Enumerator.new do |yielder| enum.each do |value| yielder.yield(value) if block.call(value) end end end p infinite_select(triangular_numbers) {|val| val % 10 == 0}.first(5) 出力結果 >|| [10, 120, 190, 210, 300]
infinite_selectのようなメソッドをEnumeratorクラスに直接追加すればより便利になる。
triangular_numbers = Enumerator.new do |yielder| number = 0 count = 1 loop do number += count count += 1 yielder.yield number end end class Enumeraotor def infinite_select(enum, &block) Enumerator.new do |yielder| enum.each do |value| yielder.yield(value) if block.call(value) end end end end p triangular_numbers .infinite_select {|val| val % 10 == 0} .infinite_select {|val| val.to_s =~ /3/ } .first(5)
出力結果
[300, 630, 1830, 3160, 3240]