16
Enumerable#lazyについて 2012/11/10 cuzic

Enumerable lazy について

Embed Size (px)

Citation preview

Page 1: Enumerable lazy について

Enumerable#lazyについて

2012/11/10

cuzic

Page 2: Enumerable lazy について

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 さんの作

Page 3: Enumerable lazy について

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個ずつ処理する。 無限個のリストでも問題なく処理できる。

Page 4: Enumerable lazy について

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

Page 5: Enumerable lazy について

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 オブジェクト

を返すように再定義されている。

Page 6: Enumerable 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

Page 7: Enumerable lazy について

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

Page 8: Enumerable lazy について

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 の内部実装まで

考えなくていい

Page 9: Enumerable lazy について

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

の便利メソッド

を使い放題

単純なイテレーション

ならいいけど・・・。

Page 10: Enumerable lazy について

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 で抽象化可能

マトリックスに対して、 行 => 列 の順で列挙するか 列 => 行 の順で列挙するか

木構造に対して、 幅優先探索を行うか 深さ優先探索を行うか

Page 11: Enumerable lazy について

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 だけ

Page 12: 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

Page 13: Enumerable lazy について

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

Page 14: Enumerable lazy について

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

Page 15: Enumerable lazy について

2012/11/10 Scala/Clojure/Ruby勉強会 「Enumerable#lazy について」

14 まとめ

Enumerable#lazy

@yhara さんの作品

無限を無限のまま扱える

次の1個だけ処理して、残りは後回し

全部一気に処理するより、必要なメモリ量が少なくて済む

Enumerator

Enumerator::Lazy の内部実装で大活躍

Enumerator は Fiber より分かりやすく、実用的。

Fiber でやりたいことの大部分は Enumerator で十分可能

発展的な使い方

メソッドチェーンでパイプライン処理とか

クロージャを使ってより遅延評価させたり。

Page 16: Enumerable lazy について

15

ご清聴ありがとう

ございました