🧝‍♀️

each_with_objectの使い方

2023/01/07に公開2

each_with_object は便利だがあまり知られていなさそう、かつ個人的には少し理解するのに時間がかかったので使い方をまとめた。

まずは公式ドキュメント見ようぜのURLを貼っておく
https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/each_with_object.html

早速、こんな時は each_with_object使えるぜ!!な例を上げていく

例1

こんな時、

ruby
langs = ["ruby", "python", "go"]
lang_hash = {}
langs.each do |lang|
  lang_hash[lang] = lang.upcase
end

return lang_hash
#=> {"ruby"=>"RUBY", "python"=>"PYTHON", "go"=>"GO"}

each_with_object を使うと

ruby
langs = ["ruby", "python", "go"]
lang_hash = langs.each_with_object({}) do |lang, hash|
  hash[lang] = lang.upcase
end

return lang_hash
#=> {"ruby"=>"RUBY", "python"=>"PYTHON", "go"=>"GO"}
lang_hash = {}

が不要になる。

例2

こんな時、

ruby
posts = [
  { id: 1, title: 'hoge-1', category: 1 },
  { id: 2, title: 'hoge-2', category: 2 },
  { id: 3, title: 'hoge-3', category: 2 },
  { id: 4, title: 'hoge-4', category: 1 },
]

# hashの初期化
post_hash_by_category_id = {}
posts.each do |post|
  # categoryごとの配列初期化
  post_hash_by_category_id[post[:category]] = [] if post_hash_by_category_id[post[:category]].nil?
  post_hash_by_category_id[post[:category]] << { title: post[:title] }
end

return post_hash_by_category_id
#=> {1=>[{:title=>"hoge-1"}, {:title=>"hoge-4"}], 2=>[{:title=>"hoge-2"}, {:title=>"hoge-3"}]}

これを each_with_object で実装すると、

ruby
posts = [
  { id: 1, title: 'hoge-1', category: 1 },
  { id: 2, title: 'hoge-2', category: 2 },
  { id: 3, title: 'hoge-3', category: 2 },
  { id: 4, title: 'hoge-4', category: 1 },
]

post_hash_by_category_id = posts.each_with_object(Hash.new { |v, k| v[k] = []}) do |post, hash|
  hash[post[:category]] << { title: post[:title] }
end

return post_hash_by_category_id
#=> {1=>[{:title=>"hoge-1"}, {:title=>"hoge-4"}], 2=>[{:title=>"hoge-2"}, {:title=>"hoge-3"}]}
# hashの初期化
post_hash_by_category_id = {}
と
# categoryごとの配列初期化
post_hash_by_category_id[post[:category]] = [] if post_hash_by_category_id[post[:category]].nil?

が不要になる。

ぜひ、手を動かして、実務でリファクタリングしてみてください。


余談1

これを書くきっかけが、実は レビューで Hash.new { |v, k| v[k] = []})使うといいよーって言われて、どんなふうに使うと良くなるかなーと考えて色々調べたら each_with_object というメソッドを使うといいらしいというところに行き着いた。(例2のパターン)

余談2

公式ドキュメントは使い方としてはいい例かもしれないが、用途としてはダメな例。
まーみればわかる通り、これ map 使った方がええやろーって感じなので、ドキュメントで書かれてるから、これで!!とはしないように・・・

each_with_objectmapを使った時の比較

evens = (1..10).each_with_object([]) {|i, a| a << i*2 }
evens = (1..10).map{ |i| i*2 }

Discussion

ピン留めされたアイテム
Junichi ItoJunichi Ito

tsubasa_ryuto さん、こんにちは。
each_with_object、僕の場合、昔のRubyではよく使ってたんですが、最近は便利メソッドが増えてきてあまり使わなくなってきましたね。。

例1 のようなケースはto_hメソッドを使った方がシンプルになります(Ruby 2.6以上なら)。

langs = ["ruby", "python", "go"]
lang_hash = langs.to_h do |lang|
  [lang, lang.upcase]
end

例2 はたしかにeach_with_objectが便利ですが、一方でeach_with_object(Hash.new { |v, k| v[k] = []})の部分がごちゃごちゃしてあまり可読性が高くない気がします。
僕だったらあえてただのループにするかもしれません。

post_hash_by_category_id = Hash.new { |v, k| v[k] = [] }
posts.each do |post|
  post_hash_by_category_id[post[:category]] << post.slice(:title)
end
post_hash_by_category_id

なお、上のコードでは{ title: post[:title] }の代わりにsliceメソッドを使ってみました。

https://docs.ruby-lang.org/ja/latest/method/Hash/i/slice.html

以上、ご参考までに😃