【Ruby】便利なEnumerableについて


## はじめに
"Enumerable" は "enumerate"（列挙する）に由来し「列挙可能な」という意味です。

`Array, Hash, Range` 等のクラスに `Enumerableモジュール` はインクルードされており、代表的なのは、 `each、map、select` 等です。実際に業務で使って便利だなと思ったものを含めて整理したいと思います。

## 便利なメソッドたち

### [include?](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/include=3f.html)
`==`の関係が成立する時にtrueを返します

```ruby
[1, 2, 3].include?(2) # => true
```

## [all?](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/all=3f.html)
全ての要素が真である時にtrueを返します。そうでない時はfalseを返します。

```ruby
[1, 2, 3].all? { |val| val > 0 } # => true
[1, 2, 3].all?(Integer) # => true
```


## [any?](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/any=3f.html)
いずれかの要素が真である場合にtrueを返却します
```ruby
[1, 2, 3].any? { |val| val > 2 } # => true
[nil, true, 1, :hoge].any?(Integer) # => true
```

## [none?](https://docs.ruby-lang.org/ja/latest/method/Array/i/none=3f.html)
`all?`の反対ですべての要素が偽である場合にtrueを返却します
```ruby
[1, 2, 3].none? { |val| val > 4 } # => true
[nil, false].none? # => true
```

## [count](https://docs.ruby-lang.org/ja/latest/method/Array/i/count.html)
引数がない時は要素の数を返します。
引数があるときはその引数に一致した数を返します。
引数がブロックである場合は、ブロック内の評価で真である数を返します。
```ruby
[1, 2, 3, 2].count # => 4
[1, 2, 3, 2].count(2) # => 2
[1, 2, 3, 2].count{ |val| val % 2 == 1 } # => 2
```

## [each_cons(n)](https://docs.ruby-lang.org/ja/latest/class/Enumerable.html#I_EACH_CONS)
要素を重複ありでn要素ずつに区切り、ブロックに渡して繰り返します。
要素間の差分を利用したロジックを組みたい時に便利です。
```ruby
[1, 2, 3, 5, 8, 13].each_cons(2).map{ |a, b| b - a } # => [1, 1, 2, 3, 5]
```

## [detect / find](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/detect.html)
ブロック内で評価して、最初に真になった要素を返します。
```ruby
[1,2,3].detect{ |val| val % 2 == 0 } # => 2
```

## [each_with_object(obj)](https://docs.ruby-lang.org/ja/latest/method/Enumerator/i/with_object.html)
与えられた任意のオブジェクトと要素をブロックに渡して処理を繰り返し、
最初に与えられたオブジェクトを返します。

ハッシュ整形前に`空ハッシュ{}`を定義する必要がなくなるのは個人的に感動でした。
```ruby
[1,2,3].each_with_object([]){ |val, list| list.push(val * 2) } # => [2, 4, 6]

[1,2,3].each_with_object({}){ |val, hash| hash[val] = (val * 2) } # => {1=>2, 2=>4, 3=>6}
```

## [filter / select / find_all](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/filter.html)
ブロック内で評価して、真になった要素全てを配列として返します。

```ruby
[1, 2, 3, 4, 5].filter { |num| num.even? } # => [2, 4]
```

## [filter_map](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/filter_map.html)
各要素に対してブロック内で評価が真になった要素全てを配列として返します。
違いはブロックがnilを返す場合、その要素は結果の配列に含まれません。

変換とフィルタリングを同時に行う際に便利です。

```ruby
[1, 2, 3, 4, 5].filter_map { |num| num.even? ? num * num : nil } # => [4, 16]
```

## [flat_map / collect_concat](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/collect_concat.html)
各要素をブロックに渡し、その返り値を連結した配列を返します。
flatten + map と挙動が異なる点に注意が必要（[こちらの記事が参考になった](https://k-koh.hatenablog.com/entry/2022/07/22/090611)）
```ruby
[[1,2], [3,4]].flat_map{|i| p i } # 各要素がブロックに渡されている
=> [1, 2]
=> [3, 4]

[[1,2], [3,4]].flat_map{|i| i.map{|num| num * 2}} # [2, 4]と[6, 8]を連結して返す
=> [2, 4, 6, 8]
```


## [max_by](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/max_by.html)
ブロックでの評価結果が最大の要素を返します。
同列１位がある場合は最初に出現した要素が選択されます。

maxよりもブロックを引数に取れるこっちを使用することが多かったです。

```ruby
["apple", "banana", "orange", "grape"].max_by { |word| word.length } # => "banana"
```

## [group_by](https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/group_by.html)
ブロック内で評価した結果をキー、対応する要素の配列を値とするハッシュを返します。

個人的にはこれが革命的で、modelインスタンスの配列を
特定のカラムでグルーピングできるのが便利でした
```ruby
["apple","111111",  "banana", "orange", "grape"].group_by { |word| word.length }
# => {5=>["apple", "grape"], 6=>["111111", "banana", "orange"]}
```

ハッシュに対しても使用できます
```ruby
users = [
    { name: '鈴木', age: 20 },
    { name: '佐藤', age: 25 },
    { name: '山田', age: 25 },
    { name: '高橋', age: 20 }]

users.group_by { |user| user[:age] }
# => {20=>[{:name=>"鈴木", :age=>20}, {:name=>"高橋", :age=>20}], 25=>[{:name=>"佐藤", :age=>25}, {:name=>"山田", :age=>25}]}
```

モデルインスタンスに対しても使用できます
```ruby
class User
  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end
end

user_1 = User.new('鈴木', 20)
user_2 = User.new('佐藤', 25)
user_3 = User.new('山田', 25)
user_4 = User.new('高橋', 20)

[user_1, user_2, user_3, user_4].group_by { |user| user.age }
# => {20=>[#<User:0x000000010485fdb8 @age=20, @name="鈴木">,
#           <User:0x000000010485fae8 @age=20, @name="高橋">],
#     25=>[#<User:0x000000010485fc50 @age=25, @name="佐藤">,
#           <User:0x000000010485fb88 @age=25, @name="山田">]}
```
また、`ActiveRecord::Relation`に対して使用することで
グーピングを行ってからその後の処理ができるので大変便利！


## 自作クラスにEnumerableをMix-in
`Enumerable` を `include` することで対象のクラスが要素を列挙できるようになります。

どういうことかというと...

[リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/Enumerable.html)
>繰り返しを行なうクラスのための Mix-in。このモジュールのメソッドは全て each を用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。

上記のことからEnumerableに必要な以下の条件になります
- そのクラスに内部イテレータメソッドに`each`と名をつけること
- `sortメソッド` のために、`<=>`比較演算子の実装を持つこと


実際に書いてみましょう


``` ruby
class Pokemon
  attr_reader :individual_value
  def initialize
    @individual_value = rand(32) # 個体値 0～31
  end

  def <=>(other)
    @individual_value <=> other.individual_value
  end
end

class PokemonCollection
  include Enumerable
  def initialize
    @pokemon_list = []
  end

  def add(pokemon)
    @pokemon_list.push(pokemon)
  end

  def each(&block)
    @pokemon_list.each(&block)
  end
end

pokemon_list = PokemonCollection.new
pokemon_list.add(Pokemon.new)
pokemon_list.add(Pokemon.new)
pokemon_list.add(Pokemon.new)

p pokemon_list.any? { |pokemon| pokemon.individual_value > 10 } 
p pokemon_list.max
```
`Enumerable` モジュールを`include` することで、`any?, all? メソッド` など、 
`each メソッド` が実装されていることが期待されるメソッドが使用できるようになります。

そして、`<=> メソッド`を追加することで `Pokemon` オブジェクト同士を比較できるようになり、`sort, max` などのメソッドにも応答できるようになります。

## 余談

`Enumerable` を使用して書きましたが、上記の例だと個体値が10より大きいという
ビジネスロジックが露出する設計になるので個人的には微妙。

```ruby
pokemon_list.any? { |pokemon| pokemon.individual_value > 10 } 
```
`PokemonCofllection`と `Pokemon` 間の依存度が上がってでも以下のように
ロジックをクラス内に包み隠す方が好み。

```ruby
class PokemonCollection
  # include Enumerable
  def initialize
    @pokemon_list = []
  end

  def add(pokemon)
    @pokemon_list.push(pokemon)
  end

  # def each(&block)
  #   @pokemon_list.each(&block)
  # end

  # コレクションクラスはPokemonクラスにindividual_valueがあることを知っている
  def exists_IV_greater_than?(num)
    @pokemon_list.any? { |pokemon| pokemon.individual_value > num }
  end
end
```
ということもあり、`Enumerable` を `include` することは体験上ほとんどないです。
これだ！という使いどころは模索中です🧐。

## 最後に
ここまで見ていただきありがとうございます。m(_ _)m

rubyには便利なメソッドが山ほど用意されているので、
使える時に積極的に使えるようになるといいですね。