Upload
tomoya-cuzic
View
1.275
Download
1
Embed Size (px)
Citation preview
Enumerable#lazyについて
2012/11/10
cuzic
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
1 Ruby 2.0.0 について
Ruby の生誕20周年にリリース予定
2013年2月24日 公式リリース予定
新機能
実はそれほど多くない
100%互換を目指している
大きいのは、パラメータ引数 と Module#prepend くらい?
Enumerable#lazy
Ruby 2.0.0 の新機能の1つ
遅延評価っぽい配列演算を扱うことができるライブラリ
Esoteric Lang や Ruby ISO 界隈で有名な @yhara さんの作
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerable#lazy 2
require 'prime' infinity = 1.0/0 (1000..infinity).select do |i| i.prime? end. # ここで、無限ループに陥る take(10).each do |i| puts i end
■ 1,000 より大きな素数を 10個 出力する
無限ループに陥る
require 'prime' infinity = 1.0/0 (1000..infinity).lazy.select do |i| i.prime? end.take(10). # 最初の10個だけを評価する each do |i| puts i end
■ 1,000 より大きな素数を 10個 出力する
最初の 10個だけが処理される
Enumerable#lazy を使うと、"遅延評価" される
ふつうにやると、無限ループになっちゃってハマる
Enumerable#lazy があれば、必要な分を1個ずつ処理する。 無限個のリストでも問題なく処理できる。
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerable#lazy のベンリなところ 3
■ 1,000 より大きな素数を 10個
【Enumerable#lazy を使わなければ】
(1) map などの処理ごとに配列を生成する ・大量の配列生成をともなうので、 メモリ効率が悪い
(2) 最終的に使われなくても、生成する
・もったいない
・下の例だと、10個目より後は不要
(3) 無限ループに陥る
・無限個の要素を処理しようとするので 無限ループになっちゃう。
【Enumerable#lazy なら】
(1) map などの処理ごとに、
Enumerator::Lazy オブジェクトを生成 ・すべての要素を配列にしたりしない
・大量のメモリを消費したりしない
(2) 次に使う分だけ生成する
・エコ
・下の例だと、10個目より後は何もしない
(3) 無限を無限のまま扱える
・宇宙ヤバい
require 'prime' infinity = 1.0/0 (1000..infinity).lazy.select do |i| i.prime? end.take(10). # 最初の10個だけを評価する each do |i| puts i end
■ ".rb" ファイルを 10個 列挙
require 'find' Find.find("/").lazy.select do |f| File.file?(f) && f.end_with?(".rb") end.take(10).each do |f| puts f end
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumeratable#lazy の内部動作(1) 4
Enumerable#lazy は Enumerator::Lazy オブジェクトを返す Enumerator::Lazy は Enumerator のサブクラス
Enumerator : 外部イテレータを実現するためのクラス 内部では Fiber を使って実装されている
Enumerator::Lazy は、Enumerable モジュールのメソッドを再定義 map 、select、take などのメソッドの返り値は、Enumerator::Lazy オブジェクト
※ Enumerable モジュールで増えるメソッドは Enumerable#lazy だけ。
infinity = 1.0/0 (1000..infinity).lazy. select do |i| i.prime? end. take(10).each do |i| puts i end
ここで、Enumerator::Lazy オブジェクトを生成。 1個ずつ処理する。
select や take などの Enumerable モジュールのメソッドは、Enumerator::Lazy クラスでは
1個ずつ処理する Enumerator::Lazy オブジェクト
を返すように再定義されている。
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerable#lazy の内部動作(2) 5
【lazy Fiber】 (1000..inf).lazy 値を順に yield する Enumerable::Lazy オブジェクトを生成
次の値を yield して、 lazy Fiber は中断する。
【select Fiber】 select {|i| i.prime?}
素数である場合だけ yield する E::Lazy オブジェクト
素数となる値が来るまで、親 Fiber に値を要求する。
素数があれば、その値を yield して select Fiber は中断
【take Fiber】 take(10)
最初の 10個であれば yield する E::Lazy オブジェクト
親 Fiber から得られた値を 10個目までは 子Fiber に
そのまま yield し、take Fiber は処理を中断する。
【each Fiber】 each {|i| puts i }
親Fiber から受け取った値をそのまま 表示する。
ループごとに親 Fiber に次の値を要求する。
Enumerable#lazy は最初の1個だけがんばって処理。残りは後回し。 2個目以降については、後工程で必要になって初めて、前工程の処理を行う。
inf = 1.0/0 (1000..inf).lazy.select do |i| i.prime? end.take(10). # 最初の10個だけ評価 each do |i| puts i end
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerator の復習
Enumerator
Enumerable なオブジェクト を簡単に作れる ~Ruby 1.8.6 の標準添付ライブラリ
Ruby 1.8.7 の組込ライブラリ Enumerable::Enumerator
Ruby 1.8.8~ の組込ライブラリ Enumerator
Ruby 1.9 で Generator の機能を 取り込み強化される
6
enum = "abc".enum_for(:each_byte) enum.each do |byte| puts byte end
■ Object#enum_for による Enumerator の生成
str = "abc" enum = Enumerator.new(str, :each_byte) enum.each do |byte| puts byte end
■ Enumerator.new による Enumerator の生成
enum = Enumerator.new do |yielder| "abc".each_byte do |byte| yielder << byte end end # Fiber を使っているから速い enum.each do |byte| puts byte end
■ Enumerator.new (Ruby 1.9 feature )
g = Generator.new do |yielder| "abc".each_byte do |byte| yielder.yield byte end end # callcc だから遅い g.each do |byte| puts byte end
■ Ruby 1.8 の添付ライブラリ Generator
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerator と Fiber 7
fib = Enumerator.new do |yielder| a = b = 1 loop do yielder << a a, b = b, a + b end end fib.each do |i| break if i > 1000 puts i end
■ Enumerator の例(フィボナッチ数)
fib = Fiber.new do a = b = 1 loop do Fiber.yield a a, b = b, a + b end end def fib.each loop do yield fib.resume end end fib.each do |i| break if i > 1000 puts i end
■ 《参考》 Fiberの例(フィボナッチ数)
Enumerator は Fiber を活用するデザインパターンの1つ Fiber における難しい概念を隠ぺい Fiber.yield => Enumerator::Yielder#<< 、 Enumerator#each による列挙
Enumerator を使いこなせれば、十分 Fiber のメリットを活用できる 《参考: Fiber にできて、Enumerator でできないこと》 親 Fiber から 子Fiber へのデータの送付(Fiber.yield の返り値の利用)
これが必要。
だけど、これが
難しい。(汗)
Enumerator の内部実装まで
考えなくていい
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerator はベンリ 8
fib = Enumerator.new do |yielder| a = b = 1 loop do yielder << a a, b = b, a + b end end fib.each do |i| break if i > 1000 puts i end fib.each_cons(2) do |i, j| break if i > 1000 puts "#{i} #{j}" end
■ Enumerator の例(フィボナッチ数)
def fibonacci a = b = 1 loop do yield a a, b = b, a + b end end
fibonacci do |i| break if i > 1000 puts i end
prev = nil fibonacci do |i| break if i > 1000 puts "#{prev} #{i}" if prev prev = i end
■ 《参考》 ブロック付メソッド呼び出しの例
Enumerator なら Enumerable の便利メソッドを使える。 ブロック付メソッド呼び出しは単純なイテレーションならいいけど。。。
Enumerator はオブジェクトなので、使い回しが簡単。 引数にしたり、返り値にしたり、インスタンス変数に格納したり
Enumerator オブジェクトの生成も慣れれば簡単。
慣れれば、
カンタン
面倒 ! !
Enumerable
の便利メソッド
を使い放題
単純なイテレーション
ならいいけど・・・。
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
(おまけ) Enumerator の利用例
1 2 3
4 5 6
7 8 9
/
/bin /usr
/usr/local /usr/lib
/etc /home
/home/cuzic
1 4 7
2 5 8
3 6 9
/
/bin /usr
/usr/local /usr/lib
/etc /home
/home/cuzic
①
② ③ ④ ⑤
⑥ ⑦ ⑧
①
② ③ ⑥ ⑦
④ ⑤ ⑧
データ構造に対する巡回方法を Enumerator で抽象化可能
マトリックスに対して、 行 => 列 の順で列挙するか 列 => 行 の順で列挙するか
木構造に対して、 幅優先探索を行うか 深さ優先探索を行うか
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerable#lazy の実装 10
module Enumerable def lazy Enumerator::Lazy.new(self) end end class Enumerator class Lazy < Enumerator def initialize(obj, &block) super(){|yielder| begin obj.each{|x| if block block.call(yielder, x) else yielder << x end } rescue StopIteration end } end
def select(&block) Lazy.new(self){|yielder, val| if block.call(val) yielder << val end } end def take(n) taken = 0 Lazy.new(self){|yielder, val| if taken < n yielder << val taken += 1 else raise StopIteration end } end …… end
Enumerable#lazy を使うと、Enumerable のメソッドの返り値が Enumerator::Lazy オブジェクトになる
Enumerable モジュールに増やすメソッドは Enumerable#lazy だけ
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
Enumerable#lazy の活用例
Enumerable#lazy を上手に活用すると生成、フィルタを順に 処理するプログラムをメソッドチェーンで簡潔に記述できる。
11
require 'forwardable' class FizzBuzz extend Forwardable def_delegators :@lazy, :each def self.each &blk fb = self.new fb.fizzbuzz.fizz.buzz. take(30).each(&blk) end def initialize inf = 1.0/0 @lazy = (1..inf).lazy end def take n @lazy = @lazy.take n self end
def map &block @lazy = @lazy.map &block self end
def fizzbuzz map do |i| (i % 15 == 0) ? "FizzBuzz" : i end end
def buzz map do |i| (i % 5 == 0) ? "Buzz" : i end end
def fizz map do |i| (i % 3 == 0) ? "Fizz" : i end end end
FizzBuzz.each do |i| puts i end
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
(おまけ)パイプライン処理
Enumerator を活用すると、UNIX のパイプのように処理を書ける 下記の例では本当にパイプ(|)で処理する例
ついカッとなってやった。今は反省している。
ふつうはメソッドチェーンで十分実用的 フィルタ処理をオブジェクトにしたいときはこういう方法もベンリかも。
Enumerator.new の書き方に精通していないと、分かりにくい。。。
Enumerable#map とか使って、書けたら分かりやすいのにな。。。
# Enumerable#lazy は無関係だけど。。。
12
class Pipeline <Enumerator attr_accessor :source def |(other) other.source = self other end end class FizzBuzz <Pipeline def initialize n, subst super() do |y| @source.each do |i| y << (i % n == 0) ? subst : i end end end end
def main fb = FizzBuzz.new(15, "FizzBuzz") buzz = FizzBuzz.new( 5, "Buzz") fizz = FizzBuzz.new( 3, "Fizz") inf = 1.0/0 sink = Pipeline.new(1..inf)
pipe = sink | fb | buzz | fizz pipe.take(30).each{|i| puts i} end if $0 == __FILE__ main end
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
(おまけ) もっと lazy に
クロージャを活用すれば、もっと評価タイミングを遅延できる
値ではなく、クロージャを要素とし、必要なときクロージャを評価する
クロージャなら、memoize も簡単に実装可能
同じ計算を2回以上しないようにできる。
# 下記の例も Enumerable#lazy と関係ない。。。
13
# memoize版。同じ計算は1度だけ。圧倒的に速い。 def add a, b memo = nil lambda do return memo if memo memo = a.() + b.() return memo end end fib = Enumerator.new do |yielder| a = b = lambda{ 1 } loop do yielder << a a, b = b, add(a, b) end end fib.take(36).each do |i| puts i.() end
# memoize しない場合。同じ計算を何度もする。 # すごい遅い。 def add a, b lambda do return a.() + b.() end end fib = Enumerator.new do |yielder| a = b = lambda{ 1 } loop do yielder << a a, b = b, add(a, b) end end fib.take(36).each do |i| puts i.() end
2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」
14 まとめ
Enumerable#lazy
@yhara さんの作品
無限を無限のまま扱える
次の1個だけ処理して、残りは後回し
全部一気に処理するより、必要なメモリ量が少なくて済む
Enumerator
Enumerator::Lazy の内部実装で大活躍
Enumerator は Fiber より分かりやすく、実用的。
Fiber でやりたいことの大部分は Enumerator で十分可能
発展的な使い方
メソッドチェーンでパイプライン処理とか
クロージャを使ってより遅延評価させたり。
15
ご清聴ありがとう
ございました