はじめに
"Enumerable" は "enumerate"(列挙する)に由来し「列挙可能な」という意味です。
Array, Hash, Range
等のクラスに Enumerableモジュール
はインクルードされており、代表的なのは、 each、map、select
等です。実際に業務で使って便利だなと思ったものを含めて整理したいと思います。
便利なメソッドたち
include?
==
の関係が成立する時にtrueを返します
[1, 2, 3].include?(2) # => true
all?
全ての要素が真である時にtrueを返します。そうでない時はfalseを返します。
[1, 2, 3].all? { |val| val > 0 } # => true
[1, 2, 3].all?(Integer) # => true
any?
いずれかの要素が真である場合にtrueを返却します
[1, 2, 3].any? { |val| val > 2 } # => true
[nil, true, 1, :hoge].any?(Integer) # => true
none?
all?
の反対ですべての要素が偽である場合にtrueを返却します
[1, 2, 3].none? { |val| val > 4 } # => true
[nil, false].none? # => true
count
引数がない時は要素の数を返します。
引数があるときはその引数に一致した数を返します。
引数がブロックである場合は、ブロック内の評価で真である数を返します。
[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)
要素を重複ありでn要素ずつに区切り、ブロックに渡して繰り返します。
要素間の差分を利用したロジックを組みたい時に便利です。
[1, 2, 3, 5, 8, 13].each_cons(2).map{ |a, b| b - a } # => [1, 1, 2, 3, 5]
detect / find
ブロック内で評価して、最初に真になった要素を返します。
[1,2,3].detect{ |val| val % 2 == 0 } # => 2
each_with_object(obj)
与えられた任意のオブジェクトと要素をブロックに渡して処理を繰り返し、
最初に与えられたオブジェクトを返します。
ハッシュ整形前に空ハッシュ{}
を定義する必要がなくなるのは個人的に感動でした。
[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
ブロック内で評価して、真になった要素全てを配列として返します。
[1, 2, 3, 4, 5].filter { |num| num.even? } # => [2, 4]
filter_map
各要素に対してブロック内で評価が真になった要素全てを配列として返します。
違いはブロックがnilを返す場合、その要素は結果の配列に含まれません。
変換とフィルタリングを同時に行う際に便利です。
[1, 2, 3, 4, 5].filter_map { |num| num.even? ? num * num : nil } # => [4, 16]
flat_map / collect_concat
各要素をブロックに渡し、その返り値を連結した配列を返します。
flatten + map と挙動が異なる点に注意が必要(こちらの記事が参考になった)
[[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
ブロックでの評価結果が最大の要素を返します。
同列1位がある場合は最初に出現した要素が選択されます。
maxよりもブロックを引数に取れるこっちを使用することが多かったです。
["apple", "banana", "orange", "grape"].max_by { |word| word.length } # => "banana"
group_by
ブロック内で評価した結果をキー、対応する要素の配列を値とするハッシュを返します。
個人的にはこれが革命的で、modelインスタンスの配列を
特定のカラムでグルーピングできるのが便利でした
["apple","111111", "banana", "orange", "grape"].group_by { |word| word.length }
# => {5=>["apple", "grape"], 6=>["111111", "banana", "orange"]}
ハッシュに対しても使用できます
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}]}
モデルインスタンスに対しても使用できます
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
することで対象のクラスが要素を列挙できるようになります。
どういうことかというと...
繰り返しを行なうクラスのための Mix-in。このモジュールのメソッドは全て each を用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。
上記のことからEnumerableに必要な以下の条件になります
- そのクラスに内部イテレータメソッドに
each
と名をつけること -
sortメソッド
のために、<=>
比較演算子の実装を持つこと
実際に書いてみましょう
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より大きいという
ビジネスロジックが露出する設計になるので個人的には微妙。
pokemon_list.any? { |pokemon| pokemon.individual_value > 10 }
PokemonCofllection
と Pokemon
間の依存度が上がってでも以下のように
ロジックをクラス内に包み隠す方が好み。
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には便利なメソッドが山ほど用意されているので、
使える時に積極的に使えるようになるといいですね。