Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: injectとeach_with_objectをうまく使い分ける(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。


  • 2018/07/24: 初版公開
  • 2021/04/01: 更新

Ruby: injectとeach_with_objectをうまく使い分ける(翻訳)

最近私がDevMemo.ioに追加した練習問題の中に、Enumerable#inject(エイリアスのreduceも使えます)やEnumberable#each_with_objectの練習問題がいくつかあり、以来どちらをどんなときに用いるべきかという簡単なガイドを書いてみたいと思っていました。

inject

Enumerable\#inject (Ruby 3.0.0 リファレンスマニュアル)

  • ミュータブル(変更可能)なオブジェクトやコレクションを操作して新しい値を返す場合はinjectの方がよい
  • 変更時に新しい値を返すイミュータブルなプリミティブ(訳注: Integerやシンボルなど)やValue Objectにも向いている

each_with_object

Enumerable#each_with_object (Ruby 3.0.0 リファレンスマニュアル)

  • オブジェクトやコンテナへのミュータブルな操作にはeach_with_objectの方がよい
    • 特にHashArrayが対象の場合
  • 作業の開始地点となる新しいオブジェクトを提供してそこでビルドする場合にも向いている
    • 既存のオブジェクトを変更したい場合にはさほど便利ではない

例1

いくつかコード例を見てみることにしましょう。オブジェクトのコレクションがひとつあり、それらを用いて新しいHashをひとつ組み立て、何らかの対応付け(mapping)操作を行いたいとしましょう。

  • 新しいオブジェクト(lower_to_upperというHash)をひとつ組み立てる
  • {}という新しい開始地点がある

each_with_objectはこのような場合に大変便利です。

lower = 'a'..'z'
lower_to_upper = lower.each_with_object({}) do |char, hash|
  hash[char] = char.upcase
end

一方、injectはそこまで便利ではありません。

lower = 'a'..'z'
lower_to_upper = lower.inject({}) do |hash, char|
  hash[char] = char.upcase
  hash    # これは省略できない
end

injectの場合は、直後のブロック呼び出しに渡すメモ化された値が直前のブロック呼び出しから返される必要があるからです(hashの初期値は{})。つまり、同じオブジェクトに対して操作を繰り返す場合であっても、渡すブロックの最後の行で必ずオブジェクトを返してやる必要があります。

一方each_with_objectのブロック呼び出しでは、メソッドの第1引数として最初に渡された同じ初期オブジェクトが常に使われます。

例2

しかし、改変したい既存のオブジェクトが既にあるとしましょう。この場合はeach_with_objectよりも単にeachで回す方が好ましいことが多いのですが、改変されたオブジェクトを返す必要があるならeach_with_objectの方がほんのわずか短くて済みます。

以下の3つの例はいずれも同じ結果を生成します。

mapping = {'ż' => 'Ż', 'ó' => 'Ó'}
lower = 'a'..'z'
lower.each do |char|
  mapping[char] = char.upcase
end
return mapping # 必要なら
mapping = {'ż' => 'Ż', 'ó' => 'Ó'}
lower = 'a'..'z'
lower.each_with_object(mapping) do |char, hash|
  hash[char] = char.upcase
end
mapping = {'ż' => 'Ż', 'ó' => 'Ó'}
lower = 'a'..'z'
lower.each_with_object(mapping) do |char|
  mapping[char] = char.upcase
end

既存のコレクションを改変するのであれば、eachが好ましいでしょう。改変されたコレクションは返す必要がないことが多いためです。つまるところ、引数としてそのオブジェクトを誰がどこから渡そうと、このオブジェクトへの参照はコード内のその場所に残ります。

例3

今度はオブジェクトの初期ステートを改変せず、常に新しいオブジェクトを生成するとしましょう。ここでの操作は常に新しいオブジェクトを返します。

最もシンプルな例は、数値の+演算子でしょう。

a = 1
b = 2

a.frozen?
# => true
b.frozen?
# => true

c = a + b
# => 3

変数aから参照される(訳注: イミュータブルな)Integerオブジェクトは、3に改変しようがありません。ここでできるのは、変数aなりbなりc別のオブジェクトを代入することだけです。

今の例は端的でしたが、Dateの場合はそこまで端的ではありません。

require 'date'
d = Date.new(2017, 10, 10)

別の日付が欲しい場合、既存のDateインスタンスは改変できません。

d.day=12
# => NoMethodError: undefined method `day=' for #<Date:

e = Date.new(2017, 10, 12)

今のは軽いジャブでしたが、injectではこれをどのように行わなければならないでしょうか。初期値がイミュータブルなオブジェクトならinjectの出番です。

(5..10).inject(:+)
(5..10).inject(0, :+)
(5..10).inject{|sum, n| sum + n }

(5..10).inject(1, :*)

以下も同様です。

starting_date = Date.new(2017,10,1)
result = [1, 10].inject(starting_date) do |date, delay|
  date + delay
end
# => Date.new(2017,10,12)

以下も同様です。

# gem install money
require 'money'
[
  Money.new(100_00, "USD"),
  Money.new( 10_00, "USD"),
  Money.new(  1_00, "USD"),
].inject(:+)
# => #<Money fractional:11100 currency:USD>

訳注

上を実行するにはgem install moneyなどでmoney gemをインストールする必要があります。

例4

今度も毎回新しいオブジェクトを作成しますが、その理由が初期ステートを改変できないからではなく、特定のメソッドが新しいオブジェクトを返すからだとしましょう。

result = [
 {1 => 2},
 {2 => 3},
 {3 => 4},
 {1 => 5},
].inject(:merge)
# => {1=>5, 2=>3, 3=>4}

Hash#mergeは2つのハッシュをマージして新しいハッシュを1つ返します。だからこそ、ここではinjectを使う方が簡単です。

each_with_objectの場合と比べてみましょう。

[
 {1 => 2},
 {2 => 3},
 {3 => 4},
 {1 => 5},
].each_with_object({}) {|element, hash| hash.merge!(element) }
# => {1=>5, 2=>3, 3=>4}

ぼやき

ブロックに渡される引数の順序がinjecteach_with_objectで逆なのってちょっとイラッときませんか?

lower_to_upper = lower.each_with_object({}) do |char, hash|
  hash[char] = char.upcase
end

lower_to_upper = lower.inject({}) do |hash, char|
  hash[char] = char.upcase
  hash
end

参考情報

DevMemo.ioでは、間もなくRubyのEnumerable学習コースを公開しますので、ぜひお試しください。気に入っていただけましたらご登録をお願いします。

関連記事

Ruby: eachよりもmapなどのコレクションを積極的に使おう(社内勉強会)

Ruby: ループには一時変数ではなくEnumerableを使おう(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。