コンテナ、ブロック、イテレータ(2) 「プログラミング Ruby 1.9 - 言語編 - 4章」
4章の続きのイテレータの実装から再開です。
- 作者: Dave Thomas with Chad Fowler and Andy Hun,まつもとゆきひろ,田和勝
- 出版社/メーカー: オーム社
- 発売日: 2010/05/26
- メディア: 単行本(ソフトカバー)
- 購入: 4人 クリック: 256回
- この商品を含むブログ (25件) を見る
イテレータの実装
Rubyで言うところのイテレータとは、コードブロックを呼び出せるメソッドのこと。
yieldを使うことで実現していきます*1。
たとえばフィボナッチ数列を出力する場合
def fib_up_to(max) i1, i2 = 1, 1 while i1 <= max yield i1 i1, i2 = i2, i1+i2 end end
と、yieldに引数を渡しておくことで、ブロック内に渡されます*2。
fib_up_to(1000) {|f| print f, " "} #=> 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
これの何がおいしいって、ブロック内とメソッドの役割を奇麗に分担できるってことにあるらしい。
次の例のArray.findの簡単な実装を見ると
class Array def find for i in 0...size value = self[i] return value if yield(value) end return nil end end
利用側は,累乗した値を条件にしてみたり
[1, 3, 5, 7, 9].find{|v| v*v > 30 } #=> 7
はてまたその逆,平方根を条件にしてみたり
[1, 3, 5, 7, 9].find{|v| Math.sqrt(v) > 2} #=> 5
と,要件だけに集中してブロック内を書けるということが便利なわけです。
# これはっ!と思う例が挙げられないのが自分の残念なところ。勉強不足ですねー :-(
特別なイテレータとしてはeach,配列を返すcollectionの他にも
ブロック通過回数を記録するeach_with_index なんてのが1.9から加わったとのこと。
inject
Enumerableモジュールにあるちょっと特殊なinjectメソッドは便利。
初期値を引数に与えて、配列の要素を加算したり
[1,3,5,7].inject(0) {|sum, element| sum + element} #=> 16
単語の配列から出現数を数えてハッシュを返したりができます
word_list = %w(ruby perl php java ruby java) map = wordlist.inject(Hash.new(0)){|h, key| h[key] += 1; h} #=> {"ruby" => 2, "perl" => 1, "php" => 1, "java" => 2}
引数を与えなかった場合,コレクションの先頭要素が初期値として与えられるので
[1,3,5,7].inject {|sum, element| sum + element} #=> 16
これでも同じ。
さらに短縮系で、引数にメソッドのシンボルを与えると
[1,3,5,7].inject(:+) #=> 16 [1,3,5,7].inject(:*) #=> 105
もう魔法のよう :-)
外部イテレータ
内部イテレータではうまく行かない場合,
たとえば二つのコレクションを並列にまわす場合はEnumeratorを使います。
to_enum (enum_for)
a = [1, 3, "cat"] b = {dog: "canine", fox: "lupine" } enum_a = a.to_enum enum_b = a.to_enum enum_a.next #=> 1 enum_b.next #=> [dog: "canine"] enum_a.next #=> 3 enum_b.next #=> [fox: "lupine"]
さらに、内部イテレータ(yieldするメソッド)に
ブロックを渡さないとEnumeratorオブジェクトを返すので
同じような処理が可能です。
enum_a = a.each enum_a.next #=> 1 enum_a.next #=> 3
loop
loopというメソッドとEnumeratorを組み合わせると,
enumeratorオブジェクトがなくなった時点で
ループを抜けるのでとても簡単に並列のループを取り扱えるようです。
short_enum = [1, 2, 3].to_enum long_enum = ('a'..'z').to_enum loop do puts "#{short_enum.next} - #{long_enum.next}" end #=> 1 - a #=> 2 - b #=> 3 - c
オブジェクトとしての enumerator
例えばeachメソッドを持たないStringクラスのオブジェクトで
each_with_indexのような処理を行いたい場合,
ブロックなしのイテレータメソッドがenumeratorオブジェクトを返すことを利用して
次のように書けます。
"cat".each_char.each_with_index {|item , index| p "#{index}: #{item}" } #=> 0: c #=> 1: a #=> 2: c
でこれはコードが読みやすくなるようにエイリアスがあります。
"cat".each_char.with_index {|item , index| p "#{index}: #{item}" }
enumeratorオブジェクトを返すメソッドなら応用可能なので
"cat".to_enum(:each_char).to_a #=> ["c", "a", "t"]
なんてことも可能です*3。
引数を取る場合は
(1..10).enum_for(:each_slice, 3).to_a #=> [1 , 2, 3], [4, 5, 6], [7, 8, 9], [10]]
ジェネレータおよびフィルタとしてのenumerator
無限列を生成するenumeratorを作ることも可能だけど,
Enumeratorの持つメソッドselectやcountはすべての要素を読み取ろうとするので,
別なメソッドを用意する必要があるとのこと。
上級者向けと注意書きがあるだけあって,いまいち理解が進んでません :-(
後でまた戻ってくるとします。
例)三角数の無限列を生成する場合
triangular_numbers = Enumerator.new do |yielder| number, count = 0, 1 loop do number += count count += 1 yielder.yield number end end 5.times {puts triangular_numbers.next} #=> 1 #=> 3 #=> 6 #=> 10 #=> 15
timesメソッドでyieldしたときにtriangular_numbersのloopが先に進み,
number をputs に渡す,という処理の流れ?
triangular_numbers はyieldしたとことで一時停止して
次の値が必要になった時(timesがyieldしたとき?)にブロックの処理が再開される...
というのが一連の流れです*5。
イディオムとしては使えても,まだ「そーなるものだからなるのよ」程度の理解です :-(
この無限列に対しても動作するselectを作るには,
Enumeratorをまた使って,triangular_numbersを回しつつ
ブロックが真となる場合に値を返すメソッドを作ればいいようです。
def infinite_select(enum, &block) Enumerator.new do |yielder| enum.each do |value| yielder.yield(value) if block.call(value) end end end p infinite_select(triangular_numbers) {|val| val % 10 == 0}.first(5) #=> [10, 120, 190, 210, 300]
この場合,firstメソッドからyieldされたときに
infinite_selectはブロックの条件が真となるところまで
triangular_numbersを回す, という流れらしいです。
書いてみたもののやっぱり理解に乏しいので再度挑戦ですっ。
4章はまだブロックの取り扱いが残っていますが,
長くなったので次回に持ち越しっ!