コンテナ、ブロック、イテレータ(2) 「プログラミング Ruby 1.9 - 言語編 - 4章」

4章の続きのイテレータの実装から再開です。

プログラミングRuby 1.9 −言語編−

プログラミングRuby 1.9 −言語編−

イテレータの実装

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]]

enum_for(to_enum)に引数を与えることができます*4

ジェネレータおよびフィルタとしての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章はまだブロックの取り扱いが残っていますが,
長くなったので次回に持ち越しっ!

*1:その他の方法は後々?

*2:追記: コピペで貼付けた出力が間違っていたのをコメントいただいたので修正

*3:"cat".split("")で良いけど

*4:この例はmap使うよりお手軽かも!

*5:理解があたってるかやや不安