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 }
これで多い日も安心。