8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Ruby】便利なEnumerableについて

Last updated at Posted at 2023-12-07

はじめに

"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

Enumerableinclude することで対象のクラスが要素を列挙できるようになります。

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

リファレンスマニュアル

繰り返しを行なうクラスのための 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 } 

PokemonCofllectionPokemon 間の依存度が上がってでも以下のように
ロジックをクラス内に包み隠す方が好み。

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

ということもあり、Enumerableinclude することは体験上ほとんどないです。
これだ!という使いどころは模索中です🧐。

最後に

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

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

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?