2011年2月22日火曜日

EnumerableモジュールとEnumeratorモジュール

rubyを使うとかなり頻繁に利用するのがEnumerableオブジェクト(イテレータ)である。
意識して使ってないけど、配列のeachメソッドやループのtimesなどがこれに該当する。

Enumerableオブジェクト

組み込みオブジェクトのArrayやHashがincludeしているモジュール。
Enumerableをincludeして、eachメソッドを定義してあげれば独自モジュールでもEnumerableが提供する様々なメソッドを利用出来る。

Arrayを使った場合のコード

array = [1, 2, 3]

# eachでループ
array.each {|x|
  puts x
}

# 添字付きでループ
array.each_with_index {|x, i|
  puts "#{i} = #{x}"
}
出力結果はこんな感じ
1
2
3
0 = 1
1 = 2
2 = 3

独自クラスの場合

class Hoge
  include Enumerable

  def initialize(num)
    @num = num
  end

  def each
    @num.each {|num|
      # prefixにhoge_を付加する。
      yield "hoge_#{num}"
    }
  end
end
出力結果はこんな感じ
Enumerableで定義されてるgrepもちゃんと使えてるのが分かる。
each start...
hoge_1
hoge_2
hoge_3
hoge_4
grep start...
hoge_2
hoge_3

ruby始めた頃は、each_with_indexなんて知らなくて、こんなループをかいてた・・・。
まぁ、動くから間違ってはないけど直感的ではないよね。一瞬なにがしたいんだ?って思ってしまう。
array.count.times {|i|
  puts "array[#{i}] = [#{array[i]}]"
}

Enumeratorオブエクト

each以外のメソッドでもEnumerableを使うためのラッパークラス
Enumeratorはイミュータブルなので、生成したあとの状態変更はできない。
なので、配列でパラメータ渡しをするのではなくEnumeratorを指定したほうが良い。

# eachメソッドを定義しているクラスのto_enumを呼び出すと、Enumeratorを生成できる。
enum = array.to_enum
enum.each {|x|
  puts x
}

# enumメソッドを定義していないクラスの場合
class Hoge
  def initialize(num)
    @num = num
  end

  def hoge
    @num.each {|num|
      yield "hoge_#{num}"
    }
  end
end

hoge = Hoge.new([1, 2, 3, 4])

# enum_forメソッドに、eachと同義のhogeメソッドを指定して Enumeratorを生成する。
enum = hoge.enum_for(:hoge)
puts "each start..."
enum.each {|x|
  puts x
}
puts "grep start..."
enum.grep(/^.*[23]$/) {|x|
  puts x
}
実行結果はこんな感じ

hogeメソッドしか持っていなくても、Enumerableのメソッドを利用できている。
each start...
hoge_1
hoge_2
hoge_3
hoge_4
grep start...
hoge_2
hoge_3