🐘

【Ruby】each_with_object と reduce(inject)の違い

2024/03/05に公開

概要

ハッシュを操作する方法として、 each_with_objectreduce(inject) の違いを解説する。
本記事ではブロックの最終評価に焦点を当てている。

最初にまとめ

各ブロックで返すもの もとのハッシュを操作するとき
each_with_object 初期値のハッシュの状態 merge! などの破壊的変更を使うときは、初期値として与えていもとのハッシュを変更していないか注意すること。
reduce(inject) ブロック内での最終評価 各ブロック内での最終評価が次の acc になるので、 merge などの非破壊的変更が使いやすい。

環境

❯ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-darwin21]

each_with_object

各ブロックで、最終的な初期値のオブジェクトの状態を返す

以下のような配列があったとします。

array = [1, 2, 3]

この配列に対して、以下の操作をします。

result = array.each_with_object({}) do |item, acc|
  acc.merge(item => item * 2)
end

あたかも上書きされているように見ますが、実際は最終的に空のハッシュを返します。

result
#=> {}

上記のコードでは、初期値として {} が渡されています。
2回目以降のループでは、初期値のハッシュの状態を返します。

各ループのブロックで merge をして初期値を上書きしているように見えますが、初期値の状態を最終的に返すので、破壊的に変更しない限り、2回目以降の acc も初期値と同じデータ構造のハッシュが返ります。

破壊的に初期値を変更に変更するため、merge!を使うと、2回目以降の acc にはそのハッシュが入ります。

array = [1, 2, 3]
result = array.each_with_object({}) do |item, acc|
  acc.merge!(item => item * 2)
end

こうすると、accは以下のようにして次のループに渡されます。
1回目のループ...{}
2回目のループ...{1=>2}
3回目のループ...{1=>2, 2=>4}

result
#=> {1=>2, 2=>4, 3=>6}

破壊的に上書きするのに = を使っても同じ結果が得られます。

array = [1, 2, 3]
result = array.each_with_object({}) do |item, acc|
  acc[item] = item * 2
end
result
#=> {1=>2, 2=>4, 3=>6}

※注意: 初期値がハッシュだと、もとのハッシュが上書きされる

original_hash = { a: 1, b: 2, c: 3 }
array = [:a, :b, :c]

result_hash = array.each_with_object(original_hash) do |item, acc|
  acc.merge!(item => acc[item] * 2)
end

上記のように、original_hash を初期値とし、array に該当するキーの値を2倍にして新しいハッシュを作成しようとするとき、

result_hash は予想通りの結果になります。

result_hash
#=> {:a=>2, :b=>4, :c=>6}

しかし、original_hash も同様に変更されてしまいます。

original_hash
#=> {:a=>2, :b=>4, :c=>6}

これは、各ループのブロック内で、original_hash に存在するキーを更新しているからです。
original_hash を1つのメソッドで1度しか使わないのであれば不具合は生じませんが、
2回目以降使用するときに初期値が変更されているので、期待通りの結果が得られません。

reduce(inject)

各ブロックで最後に評価したものを返す

reduceinject のエイリアスメソッドです。
今回は reduce に統一して説明します。

以下のような配列があったとします。

array = [1, 2, 3]

この配列に対して、以下の操作をします。

result = array.reduce({}) do |acc, item|
  acc.merge(item => item * 2)
end

reduce ではブロックで最後に評価したものが次の acc になります。
上記のコードでは最終的に初期値の {} を非破壊的に変更した結果を返すため、 acc
1回目のループ...{}
1回目のループ...{1=>2}
2回目のループ...{1=>2, 2=>4}
となります。

result
=> {1=>2, 2=>4, 3=>6}

ブロック内の最終評価を返すので、ハッシュではないもの返せます。

result = array.reduce({}) do |acc, item|
  "あああ"
end
result
#=> "あああ"

reduce を使うと、もとのハッシュを変更せずに新たなハッシュを作成しやすい

each_with_object の問題点として、もとのハッシュを変更して予期せぬ不具合が起きると説明しましたが、reduce は最終評価されたものを返すため、破壊的にハッシュを変更しなくて済むので、もとのハッシュを安全に使えます。

original_hash = { a: 1, b: 2, c: 3 }
array = [:a, :b, :c]

result_hash = array.reduce(original_hash) do |acc, item|
  acc.merge(item => acc[item] * 2)
end
result_hash
#=> {:a=>2, :b=>4, :c=>6}

original_hash
#=> {:a=>1, :b=>2, :c=>3}

Discussion