Ruby2.0ではlazyというメソッドがEnumerableモジュールに追加されるらしいよ。

yhara/enumerable-lazy · GitHub

lazyはリストに対する遅延評価を実現するメソッドなんだけど、それで意味がわからないって言うなら、それは、怠惰で短気で傲慢な君のためのメソッドだってことだから、喜んでほしいよ。

君のような怠惰で短気で傲慢な人っていうのは、よく次のようなことを言うんだよ。

君 「おい、雑誌持って来い!」
部下「どの雑誌ですか?」
君 「全部だよ!」
部下「全部って、日本で発行されてる雑誌全部ですか?」
君 「全部ったら、全部だよ、バカが」

    ...

部下「全部持って来ました。ぜぇ、ぜぇ..。全部で、えっと..」
君 「冊数なんて、どうでもいんだよ!じゃあ、そこからAKBの記事、全部切り出せ!」
部下「全部ですか... はっ、はい、分かりました...(まじか..)」

    ...

部下「はい、切り出しました。全部で、えっと..」
君 「数なんて、どうでもいいって言ってんだろ、バカが。次は、タイトルのケツに48くっ付けて、体よくしろ」
部下「はぁ。わっ、わかりました..」

    ...

部下「ぜぇ、ぜぇ、ぜぇ...。終わりました...。ぜぇ、ぜぇ、ぜぇ...」
君 「おっせーなあ。とりあえず5,6個見せろや」
部下「はい、どうぞ...」

    ...

君 「あーこれでいいや、用は足りたよ。後は捨てとけ」

部下「... (殺してやる)」

さすがに君のような人にはみんな付き合いきれないよね。で、当然にRubyもそうだったんだけど、世の中変わったんだね。2.0からは、そんな君のワガママにもRubyは答えてくれるようになるんだ。

require "enumerable/lazy"

all_magazine = 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ'

('A'..all_magazine).lazy.select { |w| w.match 'AKB' }
                        .map { |w| w.sub('AKB', '*\048*') }
                        .take(50).to_a

# => ["*AKB48*", "A*AKB48*", "*AKB48*A", "*AKB48*B", "*AKB48*C", "*AKB48*D", "*AKB48*E", "*AKB48*F", "*AKB48*G", "*AKB48*H", "*AKB48*I", "*AKB48*J", "*AKB48*K", "*AKB48*L", "*AKB48*M", "*AKB48*N", "*AKB48*O", "*AKB48*P", "*AKB48*Q", "*AKB48*R", "*AKB48*S", "*AKB48*T", "*AKB48*U", "*AKB48*V", "*AKB48*W", "*AKB48*X", "*AKB48*Y", "*AKB48*Z", "B*AKB48*", "C*AKB48*", "D*AKB48*", "E*AKB48*", "F*AKB48*", "G*AKB48*", "H*AKB48*", "I*AKB48*", "J*AKB48*", "K*AKB48*", "L*AKB48*", "M*AKB48*", "N*AKB48*", "O*AKB48*", "P*AKB48*", "Q*AKB48*", "R*AKB48*", "S*AKB48*", "T*AKB48*", "U*AKB48*", "V*AKB48*", "W*AKB48*"]

Ruby1.9で試すなら、gem install enumerable-lazyして、require "enumerable/lazy"する必要があるよ。

ここでlazyはEnumeratorのサブクラスであるLazyクラスのオブジェクトを返すよ。LazyクラスにはEnumeratorでラップしたselectやmapメソッドが定義してあって、それらが代わりに呼ばれるようになるんだ。lazyしないとselectは直ちにrangeオブジェクトに対する評価を始めるから答えが帰ってこないという事態になるけど、lazyで定義されたselectが呼ばれた場合には、それはEnumeratorでラップされていてその評価を先送りにできるんだよ。

サンプルにあった例も置いておくね。

require 'prime'

(1..1.0/0).lazy.map { |n| n**2+1 }
          .select { |n| n.prime? }
          .take(100).to_a
          
# => [2, 5, 17, 37, 101, 197, 257, 401, 577, 677, 1297, 1601, 2917, 3137, 4357, 5477, 7057, 8101, 8837, 12101, 13457, 14401, 15377, 15877, 16901, 17957, 21317, 22501, 24337, 25601, 28901, 30977, 32401, 33857, 41617, 42437, 44101, 50177, 52901, 55697, 57601, 62501, 65537, 67601, 69697, 72901, 78401, 80657, 90001, 93637, 98597, 106277, 115601, 122501, 147457, 148997, 156817, 160001, 164837, 176401, 184901, 190097, 193601, 197137, 215297, 217157, 220901, 224677, 240101, 246017, 287297, 295937, 309137, 324901, 331777, 341057, 352837, 401957, 404497, 414737, 417317, 427717, 454277, 462401, 470597, 476101, 484417, 490001, 495617, 509797, 512657, 547601, 562501, 577601, 583697, 608401, 614657, 665857, 682277, 739601]
require 'date'

date_range = Date.new(2011)..Date.new(9999)
puts date_range.lazy.select { |d| d.day == 13 and d.friday? }.take(10).to_a  
# >> 2011-05-13
# >> 2012-01-13
# >> 2012-04-13
# >> 2012-07-13
# >> 2013-09-13
# >> 2013-12-13
# >> 2014-06-13
# >> 2015-02-13
# >> 2015-03-13
# >> 2015-11-13

最初に細かいこと考えなくていいから、君のような怠惰で短気で傲慢な人にとっては、最高のツールだろ?

Enumerable#memo

で、これを参考にして簡易版のlazyというか、お手軽に遅延評価するためのメソッドEnumerable#memoを考えてみたんだよ!まあ、どこかにもうありそうだけどね…

実装は次のとおりだよ。

module Enumerable
  def memo
    Enumerator.new { |y| each { |e| yield(y, e) } }
  end
end

memoはブロックを取るんだけど、ブロックの処理の結果をその第1引数に畳み込むイメージで使うんだ。memoを使うと、先の例は次のように書けるよ。

all_magazine = 'ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ'

('A'..all_magazine).memo { |m, w| m << w if w.match 'AKB' }
                   .memo { |m, w| m << w.sub('AKB', '*\048*') }.take(50)
                   
# => ["*AKB48*", "A*AKB48*", "*AKB48*A", "*AKB48*B", "*AKB48*C", "*AKB48*D", "*AKB48*E", "*AKB48*F", "*AKB48*G", "*AKB48*H", "*AKB48*I", "*AKB48*J", "*AKB48*K", "*AKB48*L", "*AKB48*M", "*AKB48*N", "*AKB48*O", "*AKB48*P", "*AKB48*Q", "*AKB48*R", "*AKB48*S", "*AKB48*T", "*AKB48*U", "*AKB48*V", "*AKB48*W", "*AKB48*X", "*AKB48*Y", "*AKB48*Z", "B*AKB48*", "C*AKB48*", "D*AKB48*", "E*AKB48*", "F*AKB48*", "G*AKB48*", "H*AKB48*", "I*AKB48*", "J*AKB48*", "K*AKB48*", "L*AKB48*", "M*AKB48*", "N*AKB48*", "O*AKB48*", "P*AKB48*", "Q*AKB48*", "R*AKB48*", "S*AKB48*", "T*AKB48*", "U*AKB48*", "V*AKB48*", "W*AKB48*"]

つまり、ブロックの中でifを使えばselectになって、unlessを使えばrejectになって、条件分岐がなければmapになる、ってことだね。だからmemoを使ってselect, reject, mapを定義するなら次のようになるよ。

module Enumerable
  def sel
    memo { |y, e| y << e if yield(e) }
  end

  def rej
    memo { |y, e| y << e unless yield(e) }
  end

  def mp
    memo { |y, e| y << yield(e) }
  end

  def memo
    Enumerator.new { |y| each { |e| yield(y, e) } }
  end
end

残りの2つの例でmemoを使ったものも示すよ。

require 'prime'

(1..1.0/0).memo {|m, n| m << n**2+1 }
          .memo {|m, n| m << n if n.prime? }.take(100)
          
# => [2, 5, 17, 37, 101, 197, 257, 401, 577, 677, 1297, 1601, 2917, 3137, 4357, 5477, 7057, 8101, 8837, 12101, 13457, 14401, 15377, 15877, 16901, 17957, 21317, 22501, 24337, 25601, 28901, 30977, 32401, 33857, 41617, 42437, 44101, 50177, 52901, 55697, 57601, 62501, 65537, 67601, 69697, 72901, 78401, 80657, 90001, 93637, 98597, 106277, 115601, 122501, 147457, 148997, 156817, 160001, 164837, 176401, 184901, 190097, 193601, 197137, 215297, 217157, 220901, 224677, 240101, 246017, 287297, 295937, 309137, 324901, 331777, 341057, 352837, 401957, 404497, 414737, 417317, 427717, 454277, 462401, 470597, 476101, 484417, 490001, 495617, 509797, 512657, 547601, 562501, 577601, 583697, 608401, 614657, 665857, 682277, 739601]
require 'date'

date_range = Date.new(2011)..Date.new(9999)
puts date_range.memo { |m, d| m << d if d.day == 13 and d.friday? }.take(10)

# >> 2011-05-13
# >> 2012-01-13
# >> 2012-04-13
# >> 2012-07-13
# >> 2013-09-13
# >> 2013-12-13
# >> 2014-06-13
# >> 2015-02-13
# >> 2015-03-13
# >> 2015-11-13

ちょっと手軽に遅延評価できる感じで、いいよね?


(追記:2012-07-25) yharaさんのyhara/enumerable-lazy · GitHubをよく見たら、Enumerable::Lazy.newには既に上に書いたmemoの仕組みが用意してあって、しかも2.0.0-devを試してみたら、Enumerable#lazyが既にブロックを渡せるようになっていました。従って、何の苦もなく次のようにできるのでした。

('A'..all_magazine).lazy { |y, w| y << w if w.match 'AKB' }

すばらしい!


参考資料:

Route 477 - 「Enumerable#lazy」@松江RubyKaigi03

enumerabler.rb: Enumerable の遅延評価版メソッドライブラリ - まめめも


怠惰を手に入れる方法 by ウェンディ・ワッサースタイン



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS