Ruby 1.9で Enumerable#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 とが違ってくるんですね。

具体例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]

遅延評価ってホント便利です。