要約
RubocopのクラスClass: RuboCop::Cop::Style::EachWithObjectについて下記サイトのページを翻訳しました。(2019/02/24現在)
概ね直訳になるように心掛けましたが、どうしても直訳だと分かりづらい箇所は、分かりやすく翻訳しています。
記事を読んでもコード例を見ても詳しいところまでしっくり来づらかったので、その辺りの詳細を調べました。
記事の対象は、以下のような初心者の方です。
- 上記のクラスについて知りたい方
- Rubocopのドキュメントの読み方に慣れたい方
-
injectメソッドの使いどころが分からない方
翻訳
| 親クラス | Cop |
|---|---|
| インクルードしているモジュール | RangeHelp |
| 定義されているファイル | lib/rubocop/cop/style/each_with_object.rb |
概略(Overview)
この警官(cop)は、injectメソッドまたはreduceメソッドが使用されている箇所のブロックに、(訳注: メソッドの引数として)渡されたオブジェクトがそのままブロックの最後で返却(return)されているため、そのオブジェクトをブロックの最後で返却する必要のないeach_with_objectメソッドに置き換えられるかもしれないような箇所を探す。
しかしながら、アキュムレータのパラメータにブロック内で(訳注: 別のオブジェクトが)代入されている場合は、each_with_objectメソッドに置き換えることができない。
例(Examples):
# 悪い例
[1, 2].inject({}) { |a, e| a[e] = e; a }
# よい例
[1, 2].each_with_object({}) { |e, a| a[e] = e }
定数一覧(Constant Summary)
- MSG =
'Useeach_with_objectinstead of%<method>s.'.freeze
(訳注: 上記の定数の内容を日本語に訳すと、「%<method>sの代わりに、each_with_objectを使ってください」という意味になります。) - METHODS =
%i[inject reduce].freeze -
Utilモジュールからインクルードされた定数一覧
Util::LITERAL_REGEX
インスタンス属性一覧(Instance Attribute Summary)
- Copクラスから継承した属性一覧
config, #corrections, #offenses, #processed_source
インスタンスメソッド一覧(Instance Method Summary)
(メソッド数が多い上に翻訳すべき箇所も少ないため、当項目は省略します。)
コンストラクタの詳細(Constructor Details)
このクラスは、RuboCop::Cop::Copクラスからコンストラクタ(訳注: initializeメソッドのこと)を継承している。
インスタンスメソッドの詳細(Instance Method Details)
(翻訳すべき箇所が少ないため、当項目は省略します。)
翻訳は以上です。
解説
要約(Overview)部分の説明が分かりづらいと思うので、この部分について解説します。
登場する各メソッドについて
各メソッドの説明については、下記サイトが分かりやすいです。
reduceメソッドはinjectメソッドの別名なので、これ以降はinjectメソッドに絞って説明します。
概略(Overview)の説明
やはり、概略(Overview)で書かれていることが分かりづらいので、この辺りを噛み砕いていきます。
injectメソッドまたはreduceメソッドが使用されている箇所のブロックに、(訳注: メソッドの引数として)渡されたオブジェクト
このオブジェクトとは、以下のコードでいうところのinjectメソッドの引数[]のことを指しています。
a = [1, 2, 3, 4].inject([]) do |sum, n|
sum << n if (n % 2).zero?
sum
end
p a
injectメソッドの戻り値は、 ブロックが最後に返した値 です。
上記コードの出力結果は、[2,4]です。以下のような処理を行っています。
- 「nの値が偶数の時、ブロックの戻り値の配列の中にnを要素として追加する」
下記は詳細な処理内容です。- メソッドの引数に[]を渡す(メソッドの引数は初期値として使用される)
- 初期値([])と要素1(1)をブロックに渡す
- 1は奇数なので
sumに要素は追加されず、ブロックの最後でsum([])が返却される - ブロックの前回の戻り値([])と要素2(2)をブロックに渡す
- 2は偶数なので
sumに要素が追加され、ブロックの最後でsum([2])が返却される - ブロックの前回の戻り値([2])と要素3(3)をブロックに渡す
- 3は奇数なので
sumに要素は追加されず、ブロックの最後でsum([2])が返却される - ブロックの前回の戻り値([2])と要素4(4)をブロックに渡す
- 4は偶数なので
sumに要素が追加され、ブロックの最後でsum([2, 4])が返却される -
ブロックが最後に返した値
[2,4]がメソッドの戻り値となる
そのオブジェクトをブロックの最後で返却する必要のないeach_with_objectメソッドに置き換えられるかもしれない
一方、each_with_objectメソッドの戻り値は、 メソッドの引数として渡されたオブジェクト です。
以下の例を見てください。
# コード2
b = [1, 2, 3, 4].each_with_object([]) { |n, arr| arr << n if (n % 2).zero? }
p b
上記コードの出力結果は、[2,4]です。以下のような処理を行っています。
- 「nの値が偶数の時、メソッドの引数の配列の中にnを要素として追加する」
下記は詳細な処理内容です。- メソッドの引数に[]を渡す
- 要素1(1)と引数のオブジェクト([])をブロックに渡す
- 1は奇数なので
arr(引数のオブジェクト)に要素は追加されない - 要素2(2)と引数のオブジェクト([])をブロックに渡す
- 2は偶数なので
arr(引数のオブジェクト)に要素が追加される - 要素3(3)と引数のオブジェクト([2])をブロックに渡す
- 3は奇数なので
arr(引数のオブジェクト)に要素は追加されない - 要素4(4)と引数のオブジェクト([])をブロックに渡す
- 4は偶数なので
arr(引数のオブジェクト)に要素が追加される - メソッドの引数の オブジェクト
[2,4]がメソッドの戻り値となる
確かに、not_good_inject.rbのソースは、each_with_object.rbで置き換えることができます。
しかも、injectメソッドの代わりにeach_with_objectメソッドを使うことで、わざわざブロックの最後でメソッドの引数のオブジェクトを返却する必要がなくなっています。
Rubocopは、上記のようなinjectメソッドの使用について警告してくれるのです。
アキュムレータのパラメータにブロック内で(訳注: 別のオブジェクトが)代入されている場合は、each_with_objectメソッドに置き換えることができない
アキュムレータのパラメータ とは、上記コードnot_good_inject.rbで言うところの、sum(繰り返し計算で累積された値を保持している変数)のことです。
上記の説明に該当するのは、以下のようなコードです。
a = [1, 2, 3, 4].inject([]) do |sum, n|
sum << n if (n % 2).zero?
sum = 1
sum
end
p a
上記コードでは、sumに1を代入しています。出力結果は1です。
それでは、上記コードを単純にeach_with_objectに置き換えるとどうなるでしょうか。
a = [1, 2, 3, 4].each_with_object([]) do |n, sum|
sum << n if (n % 2).zero?
sum = 1
end
p a
上記コードの出力結果は、[2,4]です。 injectを使用したコードと、出力結果が異なってしまいます。
上の二つのソースはあまり意味のないソースですし、実際はアキュムレータに別のオブジェクトを代入することはないと思います。
ただ、もしアキュムレータの変数に別のオブジェクトを代入した場合、injectメソッドの最後でメソッドの引数のオブジェクトを返却しているように見えたとしても、Rubocopはinjectメソッドをeach_with_objectメソッドに置換できなくなるということです。
どういう処理になっているか詳しく知りたい場合は、lib/rubocop/cop/style/each_with_object.rbのaccumulator_param_assigned_to?メソッド(プライベートメソッド)を見てください。
それぞれのメソッドの使いどころは?
それでは、injectメソッドはどのような時に使えばよいのでしょうか。
a = [1, 2, 3, 4].inject { |sum, n| sum + n }
p a
上記コードの出力結果は、10です。以下のような処理を行っています。
- 「ブロックの戻り値に要素の値を加算する」
下記は詳細な処理内容です。- メソッドの引数がないので、要素1(1)と要素2(2)をブロックに渡す
- 要素1+要素2(3)がブロックの戻り値となる
- ブロックの前回の戻り値(3)と要素3(3)をブロックに渡す
- ブロックの前回の戻り値+要素3(6)がブロックの戻り値となる
- ブロックの前回の戻り値(6)と要素4(4)をブロックに渡す
- ブロックの前回の戻り値+要素4(10)がブロックの戻り値となる
-
ブロックが最後に返した値
10がメソッドの戻り値となる
以上のことから、各メソッドの特徴をまとめると以下のようになります。
injectメソッドの特徴
- メソッドに引数を渡すか渡さないか選択できる
- ブロックを使って要素の繰り返し計算を行える
each_with_objectメソッドの特徴
- メソッドに引数を必ず渡す
- ブロックを使ってメソッドの引数として渡されたオブジェクトを操作する
(おそらく、要素についてよりも、メソッドの引数のオブジェクトに対する操作が処理のメインになっているはずです。)
上記の特徴を念頭に置いておくと、ソースを書いている際にメソッドの使いどころが思いつくかもしれません。
あとがき
難しかったです。まだ理解が及んでいない部分もあります。
injectメソッドはちょっとややこしい気もしますが、使うことでソースがかなりすっきり読みやすくなる場合もあります。
配列の操作をしていてコードがいまひとつだなと思った時は、下記のサイトで使えそうなメソッドを探すのがよいです。