読者です 読者をやめる 読者になる 読者になる

Ruby の Enumerator でジェネレータを作ったり、遅延評価してみる

Ruby には Enumerable モジュールってのがあって、これを include したオブジェクトは、自身に対して何かしらの反復処理ができるようになる*1。 また、その反復処理を用いた Enumerable#map とか Enumerable#select *2 とか Enumerable#reduce とかが使えるようになる。

更に、Enumerator というものがある。Enumerable がモジュールなのに対して、Enumerator はクラスなので、インスタンス化できる。また、EnumeratorEnumerable を include している。この Enumerator は、外部イテレータ、いわゆるジェネレータとして使える。

フィボナッチ数列のジェネレータを作ってみよう。

fib = Enumerator.new do |y|
  a, b = 0, 1
  loop do
    y << a
    a, b = b, a + b
  end
end

fib.next # => 0
fib.next # => 1
fib.next # => 1
fib.next # => 2
fib.next # => 3

fib.take(10) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Enumerator.new にブロックを渡すと、ブロック引数として Enumerator::Yielderインスタンスが渡される。こいつの << メソッドの引数として値を渡してやると、ジェネレータとして値を返して(yield して)くれる。

更に、Enumerator::Lazy というものもある。こちらは、map flat_map select reject grep take drop zip などのメソッドが、即値で配列を返すのではなく、別の Enumerator::Lazyインスタンスを返すようにオーバーライドされている。

先程のフィボナッチ数列のジェネレータの遅延評価バージョンを作ってみる。Enumerable#lazy を呼ぶと、Enumerator::Lazyインスタンスが得られる。

lazy_fib = fib.lazy

sq = -> x { x**2 }
lazy_fib.map(&sq).select(&:odd?).first(10)
# => [1, 1, 9, 25, 169, 441, 3025, 7921, 54289, 142129]
# first は eager

lazy_fib.map(&sq).select(&:odd?).take(10)
# => #<Enumerator::Lazy: ...>
# take は lazy

遅延評価!遅延評価!🎉

*1:Enumerable モジュールの中のメソッドは、すべて each メソッドを用いて処理されるので、Enumerable モジュールを include するモジュールには、each メソッドが実装されていることが求められる。正確に言うと、この each メソッドこそが、反復処理の挙動を規定する。

*2:余談だが、filter じゃなくて select だというのをいつも忘れる。