Ruby でのイテレータを使った繰り返し #2

プログラミング言語 Ruby

プログラミング言語 Ruby

前回 (#1) に引き続き、イテレータを使った繰り返しについてまとめます。

Enumerator

Enumerator とは、汎用的な Enumerable オブジェクト *1 です。 Ruby 1.9 と 1.8.7 では組み込みなので require せずに使用可能ですが、Ruby 1.8 では require "enumerator" が必要となります。

Enumerator のクラスは Enumerable::Enumerator です。 クラスメソッド new を使って新しくインスタンスを生成できますが、普通は Object#to_enum メソッド (別名 enum_for メソッド) を使って Enumerator を取得します。

enum = [1,2,3].to_enum
puts enum.class #=> "Enumerator"
enum.each { |item| puts item } #=> "1\n2\n3\n"

もともと each メソッドを持っているオブジェクトを Enumerator にする意義は、変更不可能 (イミュータブル) な Enumerable オブジェクトを提供できるところにあります。 Array オブジェクトのままだと、どのような動作をするかわからないメソッドに引数として渡したときに勝手に要素を追加される可能性もありますが、Enumerator オブジェクトにするとそのようなことはなくなります。

to_enum メソッドは引数をとることができ、引数としてオリジナルオブジェクトの each メソッドの代わりとなるメソッド名を指定できます。 例えば、String オブジェクトは each メソッドは持っていませんが、each メソッドと同じような動作をする each_char メソッド、each_byte メソッド、each_line メソッドという 3 つのメソッドを持っています。 これらのうちいずれかを each メソッドとして使える Enumerator オブジェクトを作りたいのであれば、そのメソッドの名前 (シンボル) を to_enum メソッドの引数として渡してやります。

enum = "abcde".enum_for( :each_char )
# enum.each メソッドが "abcde".each_char メソッドの代理メソッドとなっている
enum.each { |char| puts char } #=> "a\nb\nc\nd\ne\n"

また、Ruby 1.9 ではブロックを渡さずに組み込みのイテレータメソッドを実行するという方法でも Enumerator を取得することができます。 (Ruby 1.9 では、組み込みのイテレータメソッドがブロックなしで呼び出された場合は Enumerator を返すようになっているため。)

# ブロックなしでイテレータメソッドを呼び出すと, Enumerator が返される
enum = "abcde".each_char
puts enum.class #=> "Enumerator"
# よって, 次のようなこともできる (chars メソッドは each_char メソッドの別名)
puts "abcde".chars.inject { |c1,c2| c1 + "/" + c2 } #=> "a/b/c/d/e"

Ruby 1.9 では Enumerator クラスに with_index メソッドがあり、このメソッドを使うと添字引数もブロックに渡す each メソッドを持った Enumerator クラスを取得できます。

"a".chars.each { |c,i| p i } #=> "nil"
"a".chars.with_index.each { |c,i| p i } #=> "0"

外部イテレータ

さらに、Ruby 1.9 (および 1.8.7) の Enumerator には、next メソッドが備わっており、外部イテレータとして使うこともできます。 外部イテレータというのはイテレータを使う側が繰り返しの主導権を握っているような繰り返しのことをいいます。 下の例では iteratorイテレータであり、繰り返しの処理そのものは iterator の外側が担っています (繰り返しの主導権はイテレータの外側がもっている)。 このようなものを外部イテレータといいます。 反対に、これまでみてきたようなイテレーションメソッドを使った繰り返しは、内部イテレータといいます (繰り返しの主導権はイテレーションメソッドがもっている)。

iterator = "abcd".chars
loop do
elem = iterator.next
puts elem
end

Enumerator#next メソッドは、メソッドを呼び出すごとにコレクションの中の要素を 1 つずつ返すというもので、next メソッドを繰り返し呼び出すことで要素を 1 つずつ取り出して処理することができます。 要素がなくなると、next メソッドは StopIteration 例外を起こします。 loop メソッドは StopIteration 例外を検出すると自動的に繰り返しを終了するイテレーションメソッドなので、上の例のように自然な形で記述することができます。

*1:前回説明したように、Enumerable モジュールをインクルードしているクラスのインスタンスのこと。