RubyのEnumerableを使ってみる

Ruby1.9系では Enumerable が組み込みライブラリとして用意されています。
1.8系では require "enumerator" とかしないと使えなかったのですが、*1
1.9系では明示的にロードしなくともデフォルトで使えます。*2

Enumerableについて端的に説明しておくと、

  1. Enumerableは便利である
  2. Enumerableを知らなくても、Rubyを触ったなら一度は使ったことがある
  3. Enumerableは「いかにもRubyっぽい(楽、かつ楽しい)」

今回は

  • Enumerableの基本的な説明
  • Enumerableの正体
  • Enumerableを自作クラスで使う

ということを、自分の復習がてらつらつら書いていきます。
(本当に初歩的なことなので、裏技っぽいことはまったく無いです。)
そもそもEnumerableとは何か。
簡単に言ってしまえば

# Array#each
[1, 2, 3, 4, 5].each do |value|
  puts value
end
1
2
3
4
5

# Array#map
[1, 2, 3, 4, 5].map{ |value| value.to_s } => ["1", "2", "3", "4", "5"]

# mapをもっと簡単に書くと
[1, 2, 3, 4, 5].map(&:to_s) => ["1", "2", "3", "4", "5"]

のような、 each メソッドや map メソッドのことです。
最後の呼び出しは「いちいちブロック書くのが面倒くさい」という
Rubyistの究極的な堕落っぷり(褒め言葉)をいい感じに表しています。
&の後にシンボルを渡すことで、「要素にそのメソッドを送信しろ」になるわけです。

ともあれ、これらがあるおかげで、

# 上のコードと等価 (each)
for value in [1, 2, 3, 4, 5]
  puts value
end
1
2
3
4
5

# 上のコードと非等価 (map)
strings = []
for value in [1, 2, 3, 4, 5]
  strings.push(value.to_s)
end
strings.inspect # => ["1", "2", "3", "4", "5"] 

のような書き方をせずに済みます。
mapに限って言えば一行で表現できますし、
一段上のスコープに余計な変数 strings を用意しなくてもいいので手軽です。
そもそもRubyで書いている時点で、後者の for 文はほとんど使わないと言っても過言ではないでしょう。

さらにこれらは Array クラスだけではなく、RangeクラスやHashクラスでも使えます。

(1..5).map(&:to_s) # => ["1", "2", "3", "4", "5"]
{:a => 1, :b => 2, :c => 3}.map{ |key, value| "#{key}=>#{value}" } # => ["a=>1", "b=>2", "c=>3"]

例では map だけですが、もちろんeachも使えます。
重要なのは

  • 配列、連想配列、Rangeなど、複数の要素を持つクラスに用意されていること
  • 要素ひとつひとつに対し繰り返し(≒イテレータ)を生成すること
  • これらのメソッドはそれぞれのクラスに手作業で実装されたものではなく、Enumerableを使って自動的に用意されたものであること

です。
それでは、Enumerableの実態は何なのでしょう。

Enumerableはモジュールである

Enumerableは、言ってしまえば「便利なメソッドの寄せ集め」です。
eachに始まりmapやselect、any?、find、count、max、他にもたくさんありますが、
ArrayもHashもRangeも、単純に「Enumerableをロードしているだけ」であって、
それぞれ似たような機能のメソッドを逐一実装したわけではなく、
Enumerableモジュールに処理を一任しているのです。

ちょっとその片鱗を見てみましょう。

[1,2,3].each # => #<Enumerator: [1, 2, 3]:each> 

eachにブロックを渡さずに呼び出すと、こんなのが返ってきます。
これは「[1,2,3]という配列にeachが呼び出された状態」を表す、
Enumerable::Enumeratorというクラスのインスタンスです。

Enumeratorとかいうのが出てきたけど、今度は何よ?

Enumerableはモジュールであって、クラスではありません。
そこでEnumeratorというクラスを用意することで、
もっと簡単にイテレータを生成してもらおうという開発サイドの心意気です。*3

例えば、[:a,:b,:c]を["a0", "b1", "c2"]にしたいとき、
「map_with_indexが欲しい」と思ったことがありませんか。
僕はあります。そんなとき、Enumeratorを使うと一発です。

# 配列の定義
array = [:a, :b, :c]

# Enumeratorを使わない場合
def map_with_index(collection)
  new_collection = []
  collection.each_with_index do |value, index|
    new_collection.push( yield(value,index) )
  end
  return new_collection
end
map_with_index(array) # => ["a0", "b1", "c2"]

# Enumeratorを利用する場合
[:a, :b, :c].map.with_index{ |value, index| "#{value.to_s}#{index}" } # => ["a0", "b1", "c2"]

mapもeach同様、ブロックを渡さずに呼び出すと Enumerator オブジェクトが返ります。
Enumeratorには with_index というメソッドが用意されているので、
これを利用してブロックの引数に index を受け取っています。

つまり、いちいち新しいメソッドを用意したり、
あるいはArrayクラスを拡張して Array#map_with_index というメソッドを
自力で実装しなくても、Enumerableを使えばこんなこともできるわけです。
さらに言えば、上記の map_with_index メソッドはHashには対応していません。

EnumerableモジュールとEnumeratorクラスの詳細については

を参照してください。

自作クラスでEnumerableを利用する

上記リファレンスマニュアルにある通り、Enumerableには膨大なメソッドが用意されています。
複数の要素を持てるクラス(いわゆるコレクション)でこれを利用しない手はありません。

というわけで二次元配列を扱うクラスを作ろう……と思っていましたが、
mapしたときに新しい二次元配列を用意しなきゃならないだとか、
sortどうするんだとか、考えただけで問題が山積みだったのでやめます。
本当はある程度実用的なEnumerableの使い方を書きたかったんですが、
いかんせんこのブログは行きあたりばったりなので自分でも諦めています。*4

しかし、コレクションを扱うクラスでのEnumerableの威力は高い。
導入する場合は、それぞれの要素にイテレータを適用する each メソッドを定義した上で、

class Hoge
  include Enumerable

  def initialize
    # do something ...
  end

  def each # 必須
    yield(each_element)
  end
end

こんな感じに、 include Enumerable と書くだけです。
これでmapやfind, select, any?, sort, inject, each_with_index などなど、
Enumerableに定義されているメソッドがまるごとこのクラスに定義されます。
each が必須なのは、Enumerableのメソッドが全て each を使って実装されているからです。

これはモジュールの Mix-in (組み込み)ということになりますが、
別の見方をすれば【限定的な多重継承】、あるいはRuby作者の
まつもとゆきひろさんによれば【テンプレートメソッド】とも言えます。
つまり「いちいち同じようなこと書くくらいなら新しいモジュールとクラス作る」で、
処理の抽象化(≒一般化)というプログラミングの本質にも迫るものでしょう。

今回は残念ながら応用にまでは至りませんでしたが、
誰か「Enumerableを使ったこんなのあるよ」というのがあったら教えて下さい。

*1:この時点では組み込みではなく標準添付ライブラリだったため。

*2:さらに1.8系よりも便利になっています。

*3:多分。難しい理由は僕にはわかりません。(キリッ

*4:後々友好的な使い方を思いついたら記事にするかもです。