Enumerable を include する代わりに Enumerator オブジェクトを活用する

# 概要

コレクションを扱うクラスに [Enumerable](http://docs.ruby-lang.org/ja/2.3.0/class/Enumerable.html) モジュールを include することはままあります。`each` メソッドをオーバライドするだけで、 [Enumerable#each_with_index](http://docs.ruby-lang.org/ja/2.3.0/class/Enumerable.html#I_EACH_WITH_INDEX), [Enumerable#map](http://docs.ruby-lang.org/ja/2.3.0/class/Enumerable.html#I_COLLECT), [Enumerable#inject](http://docs.ruby-lang.org/ja/2.3.0/class/Enumerable.html#I_INJECT) といった便利な定番メソッドが使えるようになるので便利です。そのため、僕自身もよく Enumerable モジュールを使っていました。

以下の `GroupedCharacters` クラスは Enumerable モジュールを include する例です。

```rb
# アニメキャラクターを作品ごとにグルーピングするためのクラス
class GroupedCharacters
  include Enumerable

  attr_reader :characters

  def initialize(*characters)
    @characters = characters
  end

  def each
    characters.group_by(&:anime).each do |anime, characters|
      names = characters.map(&:name).join(', ')

      yield(anime, names) if block_given?
    end
  end
end

Character = Struct.new(:anime, :name)

grouped =
  GroupedCharacters
    .new(
      Character.new('まどマギ', '佐倉 杏子'),
      Character.new('まどマギ', '美樹 さやか'),
      Character.new('＜物語＞シリーズ', '阿良々木 暦'),
      Character.new('＜物語＞シリーズ', '忍野 忍'),
      Character.new('ひだまりスケッチ', 'ゆの')
    )
grouped.each_with_index { |(anime, names), i| puts("(#{i + 1}) #{anime}: #{names}") }
```
```
(1) まどマギ: 佐倉 杏子, 美樹 さやか
(2) ＜物語＞シリーズ: 阿良々木 暦, 忍野 忍
(3) ひだまりスケッチ: ゆの
```

しかし、ある日偶然 [Stop including Enumerable, return Enumerator instead](http://blog.arkency.com/2014/01/ruby-to-enum-for-enumerator) (Enumerable を include するのをやめて、代わりに Enumerator を返そう) という記事を見つけました。この記事の冒頭にはこのように書かれています。

> Many times I have seen people including Enumerable module into their classes. But I cannot stop thinking that in many cases having methods such as each_with_index or take_while or minmax and many others that are available in Enumerable are not core responsibility of the class that is including them itself.

意訳すると以下のとおりです。

> Enumerable モジュールを include することはよくある。そうすることで each_with_index, take_while, min, max など数多くのメソッドが使用可能になる。しかし、それらのメソッドは include したクラスの主な責務ではないのではないか。

僕は「なるほど、一理あるな」と思いました。コレクションを扱うクラスとはいえ、それ自身に `each_with_index` や `map`, `inject` を定義するのではなく、外部イテレータである [Enumerator](http://docs.ruby-lang.org/ja/2.3.0/class/Enumerator.html) オブジェクトに委譲する方がよりスマートだと考えなおしたのです。餅は餅屋です。

そこで、記事のアドバイスをもとに前述の例を以下のように書き換えました。

```rb
require 'forwardable'

class NewGroupedCharacters
  extend Forwardable

  attr_reader :characters
  # map など Enumerable で定義されているメソッドは each に委譲する。
  def_delegators :each, *Enumerable.instance_methods(false)

  def initialize(*characters)
    @characters = characters
  end

  def each
    return enum_for(:each) unless block_given?

    characters.group_by(&:anime).each do |anime, characters|
      names = characters.map(&:name).join(', ')

      yield(anime, names)
    end
  end
end

Character = Struct.new(:anime, :name)

new_grouped =
  NewGroupedCharacters
    .new(
      Character.new('まどマギ', '佐倉 杏子'),
      Character.new('まどマギ', '美樹 さやか'),
      Character.new('＜物語＞シリーズ', '阿良々木 暦'),
      Character.new('＜物語＞シリーズ', '忍野 忍'),
      Character.new('ひだまりスケッチ', 'ゆの')
    )

new_grouped.each_with_index { |(anime, names), i| puts("(#{i + 1}) #{anime}: #{names}") }
```

`NewGroupedCharacters` クラスでは [Object#enum_for](http://docs.ruby-lang.org/ja/2.3.0/class/Object.html#I_ENUM_FOR) を使って、each をブロックなしで呼んだ場合に Enumerator オブジェクトを返すようにしています。Enumerator は Enumerable を継承しているため `each_with_index` や `map`, `inject` などのメソッドが使える上、さらに [Enumerator#with_index](http://docs.ruby-lang.org/ja/2.3.0/class/Enumerator.html#I_WITH_INDEX) などの便利なメソッドを持っています。

```rb
# new_grouped.each.each_with_index { |(anime, names), i| puts("(#{i + 1}) #{anime}: #{names}") } と同じ。
new_grouped.each.with_index(1) { |(anime, names), i| puts("(#{i}) #{anime}: #{names}") }
```

ちなみに最初に例示した `GroupedCharacters#each` の書き方では、each をブロックなしで読んだ場合の返り値が Hash オブジェクト (`@characters.group_by` の返り値) となってしまうので `with_index` が使えませんでした。

`enum_for` は初めて使ったのですが、便利ですね！おかげで [以前投稿したネタ記事] (http://qiita.com/QUANON/items/4ca81a9f5c78c4d7565f) の最後で言及している「自作クラスの `each` をブロックなしで呼んだ場合に Enumerator を返したいが、スマートに書くにはどうすればいいのか」という悩みを解決することができました。

# 追記 (2019/07)

[Kernel#DelegateClass](https://docs.ruby-lang.org/ja/latest/method/Kernel/i/DelegateClass.html) を使いましょう。

```rb
class GroupedCharacterNames < DelegateClass(Array)
  attr_reader :characters

  def initialize(*characters)
    @characters = characters
    super(grouped_character_names)
  end

  private

  def grouped_character_names
    characters.group_by(&:anime).map do |anime, characters|
      names = characters.map(&:name).join(', ')

      [anime, names]
    end
  end
end

Character = Struct.new(:anime, :name)

grouped_character_names =
  GroupedCharacterNames
    .new(
      Character.new('まどマギ', '佐倉 杏子'),
      Character.new('まどマギ', '美樹 さやか'),
      Character.new('＜物語＞シリーズ', '阿良々木 暦'),
      Character.new('＜物語＞シリーズ', '忍野 忍'),
      Character.new('ひだまりスケッチ', 'ゆの')
    )

grouped_character_names
  .each_with_index do |(anime, names), i|
    puts("(#{i + 1}) #{anime}: #{names}")
  end
```

# 参考

* [Stop including Enumerable, return Enumerator instead](http://blog.arkency.com/2014/01/ruby-to-enum-for-enumerator)