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

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: inject vs each_with_object | Arkency Blog 原文公開日: 2017/08/29 著者: Robert Pankowecki サイト: Arkency Blog 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の方がよい 特に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) … Continue reading Ruby: injectとeach_with_objectをうまく使い分ける(翻訳)