【Ruby】inject(reduce)の概要と具体例

Ruby

injectについて

injectとはリストのたたみこみ演算をするEnumableモジュールのメソッドです。
injectはEnumableモジュールに属するArrayやHashなどに適用でき、各要素に対して処理を実行して結果を算出する機能があります。
reduceinjectのエイリアス(等価のメソッド)です。

injectの構文

以下の4通りに分類してinjectの構文を紹介します。

  • 『シンボルを利用しない』かつ『初期値がない』
  • 『シンボルを利用しない』かつ『初期値がある』
  • 『シンボルを利用する』かつ『初期値がない』
  • 『シンボルを利用する』かつ『初期値がある』

『シンボルを利用しない』かつ『初期値がない』

### {} を利用する場合
オブジェクト.inject { |result, item| block }

### do...endを利用する場合
オブジェクト.inject do |result, item|
  block
end

オブジェクトの要素がitemに渡されてblockに記述された演算をします。演算結果はresultに保存されます。
演算が終わるとオブジェクトの次の要素がitemに渡されて再度blockの演算とresultへの結果の保存が行われます。(resultには各itemの演算結果が積まれていくイメージです。)
この処理がオブジェクトのすべての要素に対して行われます。

たとえば、1から10までの合計値を求める場合は以下のようになります。

# (1..10).inject { |sum, i| sum + i } でもOK
result = (1..10).inject do |sum, i|
  sum + i
end

### 結果出力
result
=> 55

eachを利用した場合は以下のようになります。

sum = 0
(1..10).each { |i| sum += i }

### 結果出力
sum
=> 55

『シンボルを利用しない』かつ『初期値がある』

### {} を利用する場合
オブジェクト.inject(初期値) { |result, item| block }

### do...endを利用する場合
オブジェクト.inject(初期値) do |result, item|
  block
end

たとえば、初期値100で、1から10までの合計値を求める場合は以下のようになります。

# (1..10).inject(100) { |sum, i| sum + i } でもOK
result = (1..10).inject(100) do |sum, i|
  sum + i
end

### 結果出力
result
=> 155

eachを利用した場合は以下のようになります。

sum = 100
(1..10).each { |i| sum += i }

### 結果出力
sum
=> 155

『シンボルを利用する』かつ『初期値がない』

各要素に対して行う演算はシンボルのみで表現できる場合もあります。

オブジェクト.inject(シンボル)

たとえば、1から10までの合計値を求める場合は以下のようになります。

result = (1..10).inject(:+)

### 結果出力
result
=> 55

『シンボルを利用する』かつ『初期値がある』

オブジェクト.inject(初期値, シンボル)

たとえば、初期値100で、1から10までの合計値を求める場合は以下のようになります。

result = (1..10).inject(100, :+)

### 結果出力
result
=> 155

injectを利用した具体例

配列を結合する

あえてinjectを利用せずとも配列の結合はできますが、injectの参考実装の1つとして紹介をします。

array1 = ['a', 'b']
array2 = ['c', 'd']
array3 = ['e', 'f']

# concatは破壊的変更を加えるため、array1の内容を変更させないために初期値([])を用意した
[array1, array2, array3].inject([], :concat)
=> ["a", "b", "c", "d", "e", "f"]

[array1, array2, array3].inject(:+)
=> ["a", "b", "c", "d", "e", "f"]

Rubyにおける配列操作の詳細解説は【Ruby】複数の配列を1つの配列に結合するメソッドまとめで紹介しています。

すべての結果が成功(true)か判定する

inject&&(論理積)と組み合わせることでall?と似たようなことを表現できます。

たとえば、配列の要素がすべて偶数ならtrue、そうでなければfalseを返す例について考えます。

all?を使った実装は以下の通りです。

[2, 4, 5].all? { |num| num.even? }
=> false

[2, 4, 6].all? { |num| num.even? }
=> true

injectを利用すると以下のように表現できます。

def all_even?(array)
  array.map do |num|
    num.even?
  end.inject(true) do |result, evaluate|
    result && evaluate
  end
end

### 結果出力
all_even?([2, 4, 5])
=> false

all_even?([2, 4, 6])
=> true

実務においては、一括更新をする際にすべての処理が成功したか判定したい時に便利です。
具体的な疑似コードは以下になります。サンプルコードはプロを目指す人のための例外処理(再)入門を参考にしています。

def extend_deadline_for_all_stakeholders(project, new_deadline)
  project.developers.map do |developer|
    # developerごとにupdateを実行(成功したらtrue, 失敗したらfalse)
    developer.update(deadline: new_deadline)
  end.inject(true) do |result, evaluate|
    # evaluate(updateの結果)が1つでもfalseならresultはfalseになる
    result && evaluate
  end
end

なお、&=(自己代入の積集合バージョン。つまりx &= yx = x & yを表現している)を利用しても『複数の処理がすべてtrueならtrue、1つでもfalseならばfalse』の実装は可能です。

def all_even?(array)
  all_even = true
  array.map do |num|
    all_even &= num.even?
  end
  all_even
end

### 結果出力
all_even?([2, 4, 5])
=> false

all_even?([2, 4, 6])
=> true

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考