Enumerable#lazy 及び Enumerator::Lazy について
lazyのポイント
省メモリであること
個人的にlazyの一番の利点はこの点だと思っています。
:
normalのコードでは、selectを呼び出した段階で1〜1000までの偶数すべてを要素とした配列がメモリ上に確保され、mapを呼び出した段階ではそれを文字列にした配列がメモリ上に確保されます。
対してlazyのコードでは、select,mapの段階でメモリ上確保されるのは、Enumerator::Lazyのインスタンスだけです。eachの段階でも、メモリ上に確保されるのは各要素1つに対してselect,mapのブロックを適用した値だけになります。無限リストに適用
Ruby1.9から利用可能なEnumeratorを利用すると簡単に無限リストが作れるようになりました。
:
fibは無限にフィボナッチ数列を返し続けるので、mapの時点で無限ループになってしまうためです。この例ではtakeとmapの順序を入れ替えれば望み通りの結果が取得できますが、lazyを使うとこの問題をもっとエレガントに解決することができます。
» ruby2.0-preview2で怠惰な生活を送ってみた。1.9版lazyもあるよ!! TECHSCORE BLOG
私としては無限リストが扱えるのが一番のポイントだと思います。
要約
map や select などのメソッドの遅延評価版を提供するためのクラス。
動作は通常の Enumerator と同じですが、以下のメソッドが遅延評価を行う (つまり、配列ではなく Enumerator を返す) ように再定義されています。
- map/collect
- flat_map/collect_concat
- select/find_all
- reject
- grep
- take, take_while
- drop, drop_while
- zip (※互換性のため、ブロックを渡さないケースのみlazy)
Lazyオブジェクトは、Enumerable#lazyメソッドによって生成されます。
Lazyから値を取り出すには、Enumerator::Lazy#force または Enumerable#first を呼びます。
class Enumerator::Lazy(Ruby 2.0.0)
Enumerable#first と Enumerable#take とが違ってくるんですね。
Ruby 1.9 で Enumerable#lazy を使えるようにするスクリプト
(thanks to » ruby2.0-preview2で怠惰な生活を送ってみた。1.9版lazyもあるよ!! TECHSCORE BLOG )
具体例1:FizzBuzz(るびまに載ってました)
lazy.rbとして保存しました。
#!/usr/bin/env ruby # coding: utf-8 load 'enumerator_lazy_ruby19.rb' fizzbuzz = Enumerator.new { |yielder| 1.upto(Float::INFINITY) do |n| case when n % 15 == 0 then yielder << "FizzBuzz" when n % 5 == 0 then yielder << "Buzz" when n % 3 == 0 then yielder << "Fizz" else yielder << n.to_s end end } fizzbuzz.first(100).each do |str| print str, ", " end puts # p fizzbuzz.map { |str| # str.upcase # }.first(100) # infinite loop puts fizzbuzz.lazy.map { |str| str.upcase }.first(100).join(", ")
実行します。
$ ruby lazy.rb 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz, Fizz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, FizzBuzz, 31, 32, Fizz, 34, Buzz, Fizz, 37, 38, Fizz, Buzz, 41, Fizz, 43, 44, FizzBuzz, 46, 47, Fizz, 49, Buzz, Fizz, 52, 53, Fizz, Buzz, 56, Fizz, 58, 59, FizzBuzz, 61, 62, Fizz, 64, Buzz, Fizz, 67, 68, Fizz, Buzz, 71, Fizz, 73, 74, FizzBuzz, 76, 77, Fizz, 79, Buzz, Fizz, 82, 83, Fizz, Buzz, 86, Fizz, 88, 89, FizzBuzz, 91, 92, Fizz, 94, Buzz, Fizz, 97, 98, Fizz, Buzz, 1, 2, FIZZ, 4, BUZZ, FIZZ, 7, 8, FIZZ, BUZZ, 11, FIZZ, 13, 14, FIZZBUZZ, 16, 17, FIZZ, 19, BUZZ, FIZZ, 22, 23, FIZZ, BUZZ, 26, FIZZ, 28, 29, FIZZBUZZ, 31, 32, FIZZ, 34, BUZZ, FIZZ, 37, 38, FIZZ, BUZZ, 41, FIZZ, 43, 44, FIZZBUZZ, 46, 47, FIZZ, 49, BUZZ, FIZZ, 52, 53, FIZZ, BUZZ, 56, FIZZ, 58, 59, FIZZBUZZ, 61, 62, FIZZ, 64, BUZZ, FIZZ, 67, 68, FIZZ, BUZZ, 71, FIZZ, 73, 74, FIZZBUZZ, 76, 77, FIZZ, 79, BUZZ, FIZZ, 82, 83, FIZZ, BUZZ, 86, FIZZ, 88, 89, FIZZBUZZ, 91, 92, FIZZ, 94, BUZZ, FIZZ, 97, 98, FIZZ, BUZZ
末尾の
first(100).join(", ")
を
take(100)
に変更すると
#<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x007fd89c0067f8>:each>>:map>:take(100)>
Enumerator::Lazy のままです。
take(100).force.join(", ")
にすると出力されます。
具体例2:末尾00の数から希望の範囲の数を抜き出す
まず末尾二桁がキレイな数の無限配列 hundreds を作ります。
当然ですが要素は単調増加します。
hundreds = Enumerator.new{ |yielder| 1.upto(Float::INFINITY) do |i| yielder << i * 100 end }
次に hundreds から、例えば10000以上で12000以下を抜き出した配列を得てみましょう。
p hundreds.lazy.drop_while { |n| n < 10000 }.take_while { |n| n <= 12000 }.force #=> [10000, 10100, 10200, 10300, 10400, 10500, 10600, 10700, 10800, 10900, 11000, 11100, 11200, 11300, 11400, 11500, 11600, 11700, 11800, 11900, 12000]
Enumerable#lazy を使ってうまくいきましたね^^
具体例3:Enumerator.take.map を Enumerator::Lazy.map.first で
Enumerator の作り方 - 別館 子子子子子子(ねこのここねこ)
の builis メソッドでのフィボナッチ数列を使います。
builis([0,1]){ |x| [x[1], x[0]+x[1]] }.take(15).map(&:first) #=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] builis([0,1]){ |x| [x[1], x[0]+x[1]] }.lazy.map(&:first).first(15) #=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
遅延評価ってホント便利です。