はじめに
この記事は、筆者が実際に業務でプルリクレビューをすると指摘することが多いEnumerableに関連するあれこれについて書こうと思います。
すべてのメソッドを網羅するわけではないので悪しからず。
英語版はこちら
Enumerable とは
繰り返しを行なうクラスのための Mix-in。このモジュールのメソッドは全てeachを用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。
https://docs.ruby-lang.org/ja/latest/class/Enumerable.html
とのことです。つまり、普段eachを使うことのあるクラス(例: Array, Hash, String)は、このEnumerableモジュールをインクルードしています。
eachを見つけたら使い所
each処理を見つけたら、eachを使わずに同じことをかける可能性があります。
一旦立ち止まってeachを使わずにかけないか探してみましょう。
また、やりたいことによっては、別のEnumerableメソッドを使うとよりスッキリする可能性があります。
実践Enumerable
難しいことは置いておいて実際に僕が指摘することが多いものをまとめたので見ていきましょう。
業務よりのものはちょっと変数に意味を持たせるなどしてみます。
Case 1: select
Before
arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |v|
new_arr << v if v.odd?
end
p new_arr # => [1, 3, 5]
After
new_arr = arr.select(&:odd?)
p new_arr # => [1, 3, 5]
Case 2: map
Before
arr = [1, 2, 3, 4, 5]
new_arr = []
arr.each do |v|
new_arr << v * 2
end
p new_arr # => [2, 4, 6, 8, 10]
After
new_arr = arr.map { |v| v * 2 }
p new_arr # => [2, 4, 6, 8, 10]
Case 3: inject
Before
arr = [1, 2, 3, 4, 5]
sum = 0
arr.each do |v|
sum += v
end
p sum # => 15
After
arr = [1, 2, 3, 4, 5]
sum = arr.inject(:+)
p sum # => 15
Case 4: any?
前提
予約ステータスが下記のように定義されている。
ステータス変更が正しいかバリデーションしたい
booking_statuses = {
pending: 0,
payment_requested: 1,
paid: 2,
cancelled: 3
}
Before
def validate_booking_transition(passed_status)
if passed_status == booking_statuses[:cancelled]
allowed = [
booking_statuses[:pending],
booking_statuses[:payment_requested],
booking_statuses[:paid]
].include?(passed_status)
elsif ...
.
.
.
end
After
def validate_booking_transition(passed_status)
if passed_status == booking_statuses[:cancelled]
allowed = %i(pending payment_requested paid).any? do |v|
passed_status == booking_statuses[v]
end
elsif ...
.
.
.
end
Case 5: group_by
Before
arr = [{code: 'a', val: 1}, {code: 'a', val: 2}, {code: 'b', val: 3}, {code: 'b', val: 4}]
new_hash = {}
arr.each do |hash|
k = hash[:code]
new_hash[k] = [] if new_hash[k].nil?
new_hash[k] << hash[:val]
end
p new_hash #=> {"a"=>[1, 2], "b"=>[3, 4]}
After
new_hash = arr.group_by { |h| h[:code] }.transform_values { |grouped_arr| grouped_arr.map { |h| h[:val] } }
p new_hash #=> {"a"=>[1, 2], "b"=>[3, 4]}
考察
それぞれについて少し考察してみましょう。
Case 1 ~ 3に関しては、よくあるeachでやってるその処理、便利メソッドですっきりかけるよというパターンです。ほぼEnumerableの紹介ですね。
Case 4は、Enumerableの使い所の話で、やりたいことによってEnumerableメソッドを使い分けることで、冗長になっているものがより簡潔にかけるようになるパターンがあります。(イメージをしやすくするためにちょっとビジネスロジック的な要素を入れています)
Case 5に関してはどうでしょうか。これに関しては、単純に一行でかけてスッキリ!な気もしますが、分かりにくくなっている気がしますね。
もっとよく見てみると, Case 1 ~ 4では、繰り返し処理に置いて、
- Case 1: 配列から条件にあった要素の配列を取り出す
- Case 2: 配列の各要素に2をかける
- Case 3: 配列の要素を足し合わせる
- Case 4: 配列の要素の中で、条件に当てはまるものがあるか判定する
というように1つのことを実行しているのに対し、Case 5では
- codeをkeyとした新しいハッシュを生成し、
- 各codeごとにvalの配列を作る
という複数の処理を行っていることが分かります(さらに言えば、配列から、ハッシュという別のオブジェクトを生成しています)
さらに詳しく見ていくと、transform_valuesのブロックの中でさらにmapを実行しています。一見シンプルに書けているようで、2重ループを作り出してしまっていますね。
このように複数のことを一回の繰り返し処理の中で行う必要がある場合、eachを使ったほうがわかりやすさ、さらに処理速度の面でも優位性がある可能性があります。
Enumerableメソッドを正しく使うメリットはなんなのか
多くの開発現場に置いて、コードを書く上で最も大切な指標は、可読性・そしてメンテナンスのしやすさではないかと思います。
eachメソッドは便利な半面、それ自体が意味を持たないため(基本的にただループを回すだけ)、時にコードの書き手が何を意図しているのかが読み手に伝わらない可能性があります。Enumerableメソッドを正しく使えば、書き手の意図が明確になり、将来のコード変更も容易になるでしょう。
可読性以外の視点でいうと、作用・副作用というものをより明確に意識できるようになると思います。が、それは長くなりそうなのでここでは割愛します。
ただし、Case 5で示したように、eachを使ったほうが結果的によいというパターンも有り得ますので(特にEnumerableメソッドの中でさらにEnumerableメソッドを使っている場合は要注意)気をつけてみましょう。
終わりに
いかがでしょうか。今回はEnumerableメソッドについて例とともに簡単に説明してみました。
使いこなせるようになってもっと気持ちよくコードを書いていきましょう;)