知ってた？ Ruby の Enumerable で意外と使えるメソッドたち


こんにちは、とまだです。

Ruby アドベントカレンダー 1 日目の記事をお届けします！

https://qiita.com/advent-calendar/2024/ruby

みなさんは普段から Ruby を使っていると思いますが、`each`や`map`以外の Enumerable メソッドってあまり使ったことないんじゃないでしょうか？
（自分も最初は`each`と`map`しか使ってなかったので、偉そうなことは言えませんが...）

実は`Enumerable`モジュールには、**めちゃくちゃ便利なメソッド**がたくさん隠れています。

今回は、**使える × 面白い**の二軸で選んだメソッドをご紹介します！

## 1. 意外と演算子だったメソッドたち

まずは、数学っぽい演算をしてくれるメソッドを紹介します。

### chain でコレクションを繋げる

よく、配列を結合したいときがありますよね。
そんなとき、`chain`を使うと便利です。

```ruby
[1, 2].chain([3, 4]) #=> Enumerator: [1, 2, 3, 4]
```

`chain`は、複数のコレクションを繋げて一つの`Enumerator`を返してくれます。

一方、`chain` を使わない場合は、以下のように書くことができます。

```ruby
# 配列の結合
[1, 2] + [3, 4]

# もしくは
[1, 2].concat([3, 4])
```

「あれ？`+`や`concat`と同じじゃない？」と思った方、鋭いですね！

実は`chain`は`Enumerator`を返すので、必要になるまで実際の配列は作られません。

```ruby
# chainを使うと
result = [1, 2].chain([3, 4])
# この時点では新しい配列は作られていない

# 実際に使うときに初めて配列が作られる
result.to_a #=> [1, 2, 3, 4]
```

言い換えると、`chain`を使うとメモリ効率がよくなるということです。

大きなデータを扱うときには、`chain`を使うといいかもしれませんね！

### product で直積を取る

では次に、`product`メソッドを紹介します。

```ruby
[1, 2].product(['a', 'b'])
#=> [[1, "a"], [1, "b"], [2, "a"], [2, "b"]]
```

これは、組み合わせを全部作ってくれるメソッドです。

たとえば、商品の色とサイズの全組み合わせを作るときに使えます。

`product`を使わない場合は、以下のように書くことができます。

```ruby
colors = ['Red', 'Blue']
sizes = ['S', 'M', 'L']
combinations = []
colors.each do |color|
  sizes.each do |size|
    combinations << [color, size]
  end
end
#=> [["Red", "S"], ["Red", "M"], ["Red", "L"], ["Blue", "S"], ["Blue", "M"], ["Blue", "L"]]

# もしくは
colors.flat_map {|color|
  sizes.map {|size| [color, size]}
}
```

上記のように、`each`や`map`を使っても同じ結果が得られますが、`product`を使うともっと簡潔に書けます。

```ruby
# 商品の色とサイズの全組み合わせを作る
colors = ['Red', 'Blue']
sizes = ['S', 'M', 'L']
colors.product(sizes)
```

同じ結果を得られるなら、短くすっきり書ける方がいいですね！

## 2. ゲーム開発で使えそうなメソッドたち

次は、ゲーム開発で使えそうなメソッドを紹介します。

### repeated_permutation で同じ要素を使った順列

`repeat_permutation`は、同じ要素を使って順列を作ってくれるメソッドです。

「順列」とは、要素の順番が重要な組み合わせのことです。

```ruby
[1, 2, 3].repeated_permutation(2)
#=> [[1,1], [1,2], [1,3], [2,1], [2,2], [2,3], [3,1], [3,2], [3,3]]
```

たとえば、サイコロを 3 回振るパターンを全部列挙したいときに使えます。

普通なら、こうでしょうか。

```ruby
dice = [1, 2, 3, 4, 5, 6]
patterns = []
dice.each do |d1|
  dice.each do |d2|
    dice.each do |d3|
      patterns << [d1, d2, d3]
    end
  end
end
```

ネストが深くなってしまいますね。

これも`repeated_permutation`を使えば、一行で書けます。

```ruby
[1, 2, 3, 4, 5, 6].repeated_permutation(3)
```

### slice_when で連続する要素をグループ化

```ruby
[1,1,2,2,2,3,4,4,4].slice_when {|a,b| a != b}
#=> [[1,1], [2,2,2], [3], [4,4,4]]
```

これは連続する同じ要素をグループ化してくれるメソッドです。

たとえば、マッチ 3 系のゲームで同じ色のブロックが何個連続しているか数えるときに使えます。

あえて `slice_when` を使わない場合は、以下のように書くことができます。

```ruby
numbers = [1,1,2,2,2,3,4,4,4]
result = []
current_group = [numbers.first]

numbers[1..-1].each do |n|
  if current_group.last == n
    current_group << n
  else
    result << current_group
    current_group = [n]
  end
end
result << current_group
```

かなり長くなってしまいますね。

`slice_when`を使えば、一行で同じことができます。

```ruby
[1,1,2,2,2,3,4,4,4].slice_when {|a,b| a != b}
```

## 3. データ分析で重宝するメソッドたち

次は、データ分析で使えるメソッドを紹介します。

### tally で要素をカウント

`taggly`は、各要素の出現回数をカウントしてくれるメソッドです。

Python を使っている人なら、`collections.Counter`に似ているかもしれません。

```ruby
['a', 'b', 'a', 'c', 'a'].tally
#=> {"a"=>3, "b"=>1, "c"=>1}
```

これは各要素の出現回数をカウントしてくれるメソッドです。
(Ruby 2.7 から使えるようになりました。)

もちろん、`each`や`transform_values`を使っても同じことができます。

```ruby
array = ['a', 'b', 'a', 'c', 'a']
counts = Hash.new(0)
array.each { |element| counts[element] += 1 }

# もしくは
array.group_by(&:itself).transform_values(&:size)

# もしくは
array.each_with_object(Hash.new(0)) { |element, counts|
  counts[element] += 1
}
```

ただ、それなりに自分で書く必要があります。
一方、`tally`を使えばこれが一行で書けます。

```ruby
['a', 'b', 'a', 'c', 'a'].tally
```

### chunk で連続する要素をまとめる

続いて、`chunk`メソッドを紹介します。

`chunk`は、連続する同じ要素をまとめてくれるメソッドです。

```ruby
[1, 2, 2, 3, 3, 3].chunk(&:itself)
#=> [[1, [1]], [2, [2, 2]], [3, [3, 3, 3]]]
```

これは連続する同じ要素をまとめてくれるメソッドです。

たとえば、株価の上昇・下降トレンドを分析する場面を想像してみましょう。

普通なら、こうでしょうか。
ここでは、連続する同じ要素をまとめる処理を自分で書いています。

```ruby
prices = [100, 110, 120, 115, 105, 95, 100, 110]
trends = []
current_trend = nil
current_values = []

prices.each_cons(2) do |a, b|
  trend = a < b ? :up : :down

  if current_trend == trend
    current_values << [a, b]
  else
    trends << [current_trend, current_values] if current_trend
    current_trend = trend
    current_values = [[a, b]]
  end
end
trends << [current_trend, current_values]
```

これを、`chunk`を使えばもっとすっきり書けます。

```ruby
prices = [100, 110, 120, 115, 105, 95, 100, 110]
prices.each_cons(2).chunk { |a, b| a < b ? :up : :down }
```

## 4. コードゴルフが捗るメソッドたち

コードをより短く書きたい人におすすめのメソッドを紹介します。

「コードゴルフ」とは、プログラムをできるだけ短く書くことを指します。

### sum は文字列連結もできる

よく合計値を求めるために使う`sum`メソッドですが、実は文字列連結にも使えます。

```ruby
['Hello', ' ', 'World'].sum("")
#=> "Hello World"
```

文字列を連結するメソッドとして使えます。

普通なら、こうでしょうか。

```ruby
['Hello', ' ', 'World'].join
# もしくは
['Hello', ' ', 'World'].reduce(:+)
```

`sum`を使うと、初期値を指定して文字列連結ができます。

```ruby
# 空文字列を初期値として指定
['Hello', ' ', 'World'].sum("")
```

### flat_map は map して flatten するのと同じ

次に、`flat_map`メソッドを紹介します。

`flat_map`は、`map`してから`flatten`するのと同じことができます。

```ruby
[[1,2], [3,4]].flat_map {|a| a.map {|n| n * 2}}
#=> [2, 4, 6, 8]
```

これは配列をフラットにしながら map する操作です。

もちろん、`map`してから`flatten`することもできます。

```ruby
# mapしてからflatten
[[1,2], [3,4]].map {|a| a.map {|n| n * 2}}.flatten

# もしくは
result = []
[[1,2], [3,4]].each do |arr|
  arr.each do |n|
    result << n * 2
  end
end
```

`flat_map`を使えば一回のイテレーションで済むので、パフォーマンス的にも有利です。

```ruby
[[1,2], [3,4]].flat_map {|a| a.map {|n| n * 2}}
```

## 5. 実務で使える便利メソッドたち

最後に、実務でよく使う（であろう）メソッドを紹介します。

### grep_v は条件に合わないものを抽出

よく、特定の条件に合わない要素を抽出したいときがありますよね。

そんなときに使えるのが、`grep_v`メソッドです。

```ruby
numbers = [1, 2, 3, 4, 5]
numbers.grep_v(1..3) #=> [4, 5]
```

これは条件に「マッチしない」要素を抽出するメソッドです。

たとえば、1 から 3 までの数値を除外したいときに使えます。

```ruby
numbers = [1, 2, 3, 4, 5]
numbers.reject { |n| (1..3).include?(n) }

# もしくは
numbers.select { |n| !(1..3).include?(n) }
```

`grep_v`を使うと、より意図が明確になります。

```ruby
# 無効なメールアドレスを見つける
emails = ["test@example.com", "invalid", "user@host"]
emails.grep_v(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
```

### group_by でハッシュにグループ化

これもよく使うメソッドです。

```ruby
[1, 2, 3, 4, 5, 6].group_by { |n| n % 3 }
#=> {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}
```

これは要素をグループ化してハッシュにしてくれるメソッドです。

たとえば、ユーザーを年齢層でグループ分けしたいときに使えます。

```ruby
numbers = [1, 2, 3, 4, 5, 6]
result = Hash.new { |h, k| h[k] = [] }
numbers.each do |n|
  result[n % 3] << n
end
```

`group_by`を使えば、一行でグループ化できます。

```ruby
# ユーザーを年齢層でグループ分け
users = [
  OpenStruct.new(name: "Alice", age: 25),
  OpenStruct.new(name: "Bob", age: 31),
  OpenStruct.new(name: "Charlie", age: 28)
]
users.group_by { |user| user.age / 10 * 10 }
```

## まとめ

いかがでしたか？

普段使っている`each`や`map`でも実現できる処理が、Enumerable の便利メソッドを使うともっと簡潔に書けることがわかりましたね。

まだまだ紹介しきれていない面白いメソッドがたくさんあります。

使えると思ったメソッドがあれば、ぜひ実際のコードで試してみてください！

## 他にもアドベントカレンダー記事を書いています！

他にも、2024 年のアドベントカレンダーに参加しています。

以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです！

https://qiita.com/tomada/items/025b3745df8c484f5856