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]