RubyのEnumerableを遅延評価にしてみる

最近、無意識のうちに遅延評価を前提としたコードを書くようになってきました。趣味の scala コードばかりを書いている弊害でしょうか。

そんな遅延脳が失態をやらかしました。正格評価前提の言語(Ruby)で、遅延評価を期待したコードを書いてしまい、プログラムをハングアップさせてしまいました。

# 正の偶数を小さい順に5個表示したい
(1..(1.0/0.0)).select(&:even?).take(5).each { |x| puts x }
# しかし、このコードは意図したとおりには動かない

Ruby の Enumerable#select は正格評価なので、select した時点で自然数から偶数全てを抽出した無限大の配列を作ろうとしてハングアップしてしまうんですね。。。

Rubyでも遅延評価できたらいいなあ。例えば 下記の scala コードのように。

// 正の偶数を小さい順に5個表示する
Iterator.from(1) filter {_ % 2 == 0} take 5 foreach println
// これは意図通りに動く

そこで、Ruby で遅延評価をするメソッド lazy_* を付け足してみました。

module Enumerable
  def self.make_lazy(*syms)
    syms.each do |sym|
      class_eval <<-"EOD"
        def lazy_#{sym}(*arg, &blk)
          Enumerator.new do |e|
            each do |x|
              [x].#{sym}(*arg, &blk).each { |y| e << y }
            end
          end
        end
      EOD
    end
  end
  
  #-- Enumeratorを返すメソッドを作成
  make_lazy :collect, :map, :select, :reject, :grep
  make_lazy :find_all, :flat_map, :concat_collector
end
 
#-- 遅延評価により無限リストでもOK
(1..(1.0/0.0)).lazy_map(&:even?).take(5).each { |x| puts x }
(1..(1.0/0.0)).lazy_select(&:even?).take(5).each { |x| puts x }
 
#-- 標準メソッドではアウト
#(1..(1.0/0.0)).map(&:even?).take(5).each { |x| puts x }
#(1..(1.0/0.0)).select(&:even?).take(5).each { |x| puts x }

これで多い日も安心。