RubyのEnumeratorとEnumerator::怠惰(Lazy)の使い所とベンチマーク
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を使うと使う時になってからちょこちょことメモリを割り当てているので少し遅くなる。しかしメモリ領域をいっきに消費しないというメリットを考慮に入れれば、交換条件として悪くはない結果。