ジャバ・ザ・ハットリ
Published on

RubyのEnumeratorとEnumerator::怠惰(Lazy)の使い所とベンチマーク

Authors
  • avatar
    ジャバ・ザ・ハットリ

Ruby の Enumerator と Enumerator::Lazy の使い所とベンチマークをまとめた。使うと意外と便利なのが Enumerator。

Enumerator の基礎動作

irb を起動して配列の each の後にブロックを渡さないでおくと、それはそのまま Enumerator オブジェクトにして返される。

$ irb
irb(main):001:0> e = [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>

Enumerator に next とやれば、順番に中の要素を出す。便利。

irb(main):002:0> e.next
=> 1
irb(main):003:0> e.next
=> 2
irb(main):004:0> e.next
=> 3

念のために言っておくと Array に next はない。

irb(main):001:0> array = [1,2,3]
=> [1, 2, 3]
irb(main):002:0> array.next
NoMethodError: undefined method `next' for [1, 2, 3]:Array

[1,2,3]と3つしかない Enumerator で最後まで来てさらに next すると、こうなる。

irb(main):005:0> e.next
StopIteration: iteration reached an end
        from (irb):5:in `next'
        from (irb):5
        from /.rbenv/versions/2.3.1/bin/irb:11:in `<main>'

その際には rewind とすればまた最初に戻せる。

irb(main):006:0> e.rewind
=> #<Enumerator: [1, 2, 3]:each>
irb(main):007:0> loop { puts e.next }
1
2
3
=> [1, 2, 3]

e = [1,2,3].each として作った Enumerator は to_enum でも Enumerator.new でも作成可能。

irb(main):008:0> e = [1,2,3].to_enum
=> #<Enumerator: [1, 2, 3]:each>
irb(main):009:0> e.next
=> 1
irb(main):010:0> e.next
=> 2
irb(main):011:0> e.next
=> 3
irb(main):013:0> e = Enumerator.new([1,2,3], :each)
(irb):13: warning: Enumerator.new without a block is deprecated; use Object#to_enum
=> #<Enumerator: [1, 2, 3]:each>
irb(main):014:0> e.next
=> 1
irb(main):015:0> e.next
=> 2
irb(main):016:0> e.next
=> 3

peek で中をのぞける。

irb(main):020:0> e.peek
=> 1
irb(main):021:0> e.peek
=> 1
irb(main):022:0> e.next
=> 1
irb(main):023:0> e.peek
=> 2
irb(main):024:0> e.peek
=> 2
irb(main):025:0> e.next
=> 2
irb(main):026:0> e.next
=> 3

each.with_index の Enumerator を作れば index が入る。

irb(main):027:0> e = %w{this is a test}.each.with_index
=> #<Enumerator: #<Enumerator: ["this", "is", "a", "test"]:each>:with_index>
irb(main):028:0> e.next
=> ["this", 0]
irb(main):029:0> e.next
=> ["is", 1]
irb(main):030:0> e.next
=> ["a", 2]
irb(main):031:0> e.next
=> ["test", 3]

Enumerator::Lazy の基礎動作

Enumerator との違いは必要になってから準備しますよ、という怠惰な方法。

例えば1から果てしなくデカい数字までの範囲を infinite_range として、その Enumerator を作る。

irb(main):032:0> infinite_range = (1..Float::INFINITY)
=> 1..Infinity
irb(main):033:0> e = infinite_range.each
=> #<Enumerator: 1..Infinity:each>
irb(main):034:0> e.next
=> 1
irb(main):035:0> e.next
=> 2
irb(main):036:0> e.next
=> 3

もしこの果てしなく続く範囲の数字の中から3と5で割り切れる数を取り出す、となると果てしなくある数字から取り出すのでずっと終わらず、強制終了しないと止まらない。

irb(main):038:0> infinite_range.select {|n| n % 3 == 0 && n % 5 ==0 }
^X^CIRB::Abort: abort then interrupt!
        from (irb):38:in `block in irb_binding'
        from (irb):38:in `each'
        from (irb):38:in `select'
        from (irb):38
        from .rbenv/versions/2.3.1/bin/irb:11:in `<main>'

Lazy を使うと、とりあえず Enumerator の用意はするが、数字を出すのは 必要になってから になる。なので無限ループには入らない。

irb(main):040:0> e = infinite_range.lazy.select {|n| n % 3 == 0 && n % 5 ==0 }
=> #<Enumerator::Lazy: #<Enumerator::Lazy: 1..Infinity>:select>
irb(main):041:0> e.next
=> 15
irb(main):042:0> e.next
=> 30
irb(main):043:0> e.next
=> 45
irb(main):044:0> e.next
=> 60
irb(main):045:0> e.next
=> 75

これは巨大なデータを扱う際にいっきにメモリーを確保しなくても動く。少ないメモリ消費量で巨大なデータを扱う際に使える技になる。

Enumerator と Enumerator::Lazy のベンチマーク

1 から 10000000 までの偶数だけ取り出して全部足す作業をする際の Enumerator と Enumerator::Lazy の違いをとった。

ベンチマークのコード

require 'benchmark'

Benchmark.bm(10) do |b|
  b.report "eager load" do
    (1..10000000).select {|n| n % 2 == 0 }.inject(:+)
  end

  b.report "lazy load" do
    (1..10000000).lazy.select {|n| n % 2 == 0 }.inject(:+)
  end
end

ベンチマークの結果


                 user     system      total        real
eager load   1.310000   0.020000   1.330000 (  1.348951)
lazy load    2.060000   0.000000   2.060000 (  2.070888)

lazy を使うと使う時になってからちょこちょことメモリを割り当てているので少し遅くなる。しかしメモリ領域をいっきに消費しないというメリットを考慮に入れれば、交換条件として悪くはない結果。

関連記事