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
の方がよい- 特に
Hash
やArray
が対象の場合
- 特に
- 作業の開始地点となる新しいオブジェクトを提供してそこでビルドする場合にも向いている
- 既存のオブジェクトを変更したい場合にはさほど便利ではない
例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}
ぼやき
ブロックに渡される引数の順序がinject
とeach_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学習コースを公開しますので、ぜひお試しください。気に入っていただけましたらご登録をお願いします。
概要
原著者の許諾を得て翻訳・公開いたします。