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

概要

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

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

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

inject

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

each_with_object

  • オブジェクトやコンテナへのミュータブルな操作には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を使おう(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ