【JavaScript】Array.prototype.reduce()、何がreduce(減らす)なのか。

2024/03/02に公開

はじめに

ナイトウ(@engineer_naito)と申します。

JavaScriptの勉強をしているのですが、本当に難しいことばかりです。
今回は最近ハマった Array.prototype.reduce() について記事にしようと思います。

ぼく「Array.prototype.reduce() って何?」
ぼく「何をreduceしてるの?」

TL;DR

Array.prototype.reduce() は、配列の各要素に対して縮小関数を実行し、その結果を単一の値にまとめるためのJavaScriptのメソッドです。
reduceは「減らす」ではなく、関数型プログラミングにおける 「畳み込み」 を意味しています。

Array.prototype.reduce()

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

reduce() は Array インターフェイスのメソッドで、配列のそれぞれの要素に対して、ユーザーが提供した「縮小」コールバック関数を呼び出します。

コールバックの初回実行時には「直前の計算の返値」は存在しません。 初期値が与えらえた場合は、代わりに使用されることがあります。 そうでない場合は、配列の要素 0 が初期値として使用され、次の要素(0 の位置ではなく 1 の位置)から反復処理が開始されます。

const array1 = [1, 2, 3, 4];

// 0 + 1 + 2 + 3 + 4
const initialValue = 0;
const sumWithInitial = array1.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue,
);

console.log(sumWithInitial);
// Expected output: 10

配列 array1 の要素を初期値 initialValue にどんどん加えていき、その結果を返しています。

構文

reduce(callbackFn)
reduce(callbackFn, initialValue)

callbackFn

callbackFn には引数を4つまで引数を取ることができます。

順番 名前 役割
第一引数 accumurator 前回の callbackFn の結果
第二引数 currentValue 現在の要素
第三引数 currentIndex 現在の位置
第三引数 array reduce が呼び出された配列

initialValue

callbackFn が最初に呼び出された際、accumulator が初期化される値です。

返り値

配列全体にわたって「縮小」コールバック関数を実行した結果の値です。

実際の使われ方

Vue公式コード例集より
https://vuejs.org/examples/#grid

<script setup>
// ...
// props.columns は ['name', 'power']
const sortOrders = ref(
  props.columns.reduce((o, key) => ((o[key] = 1), o), {})
);
// ...
</script>

このコードはVueのAPIを用いているので構造をVanillaJSに抜き出します。

// columns は ['name', 'power']
const sortOrders = columns.reduce((o, key) => ((o[key] = 1), o), {});

JS素人のぼくはこれ見たときかなりビビりました。

sortOrders{'name': 1, 'power': 1} というオブジェクトになります。
ぼくは全くわかりませんでした。

なぜ難しいのか

このメソッドについてたくさん記事があります。

https://qiita.com/seira/items/5df10748fa35dd969681

https://qiita.com/rokumura7/items/cdfc92dba508bbfb6127

https://zenn.dev/nekoniki/articles/07c09eb6811c85a753de

難しいと感じている人が多いのでしょう。
Array.prototype.reduce() が難しいと感じてしまうのはなぜでしょうか?
以下のような理由が考えられます。

  • コールバック引数を取る関数(高階関数)が難しい
  • 万能である(と評価されている)
  • reduceという言葉が難しい(直感的ではない)

コールバック引数を取る関数が取らないものより難しいのは当たり前だと思うので置いておきます。

2つ目の理由「万能」とはどういうことなのか。
先ほど挙げた記事でも言及されていますが、 map, some, forEach, filter などを reduce を用いて書き換えることができるためです。
これが「万能」たる所以なのですが、これが逆に器用貧乏のような感じに繋がっている気がします。
ぼくも「forEach と何が違うんだ?」という疑問が頭に浮かんだまま reduce の勉強を続けています。

最後の理由がぼくの理解を一番遅らせた理由です。
reduce、減らすという意味ですよね。
ぼくは小学生のときにこの言葉を初めて知りました。
テレビで見たのか社会の教科書に載っていたのか、、、
(「3R運動」(リデュース、リユース、リサイクル))

単語の意味を知っていたのですが、reduce() が何を減らしているのかがよくわからなかったんですよね。

https://teratail.com/questions/117960

プログラミングにおける"reduce"

実はプログラミングにおける"reduce"は、 「畳み込み」 を表します。

「畳み込み」関数型プログラミングにおける中心的な概念だそうです。

関数型プログラミングにおける"reduce"の意味で、Array.prototype.reduce() は使われているのですね。
知識がないのでわかりませんが、きっと「畳み込み」も"reduce"の基本的な意味である「減らす」に由来しているのでしょう。

ちなみに

海外エンジニアの方も Array.prototype.reduce() を難しいと思っている方もいらっしゃるようです。

https://dev.to/trusktr/you-dont-need-arrayreduce-557f

https://dev.to/nickyhajal/demystifying-arrayreduce-how-to-understand-it-and-when-to-use-it-in-your-code-5b96

まとめ

Array.prototype.reduce() は、配列の各要素に対して縮小関数を実行し、その結果を単一の値にまとめるためのJavaScriptのメソッドでした。
reduceは関数型プログラミングにおける 「畳み込み」 を意味しているようです。
"reduce"の意味「減らす」にこだわると、かえって混乱してしまいます。
(公式ドキュメントの日本語版では「縮小」という言葉を使っていますが、、、)

最後に

思えば"run"もいろんな意味があります。
「走る」、「経営する」、「出馬する」、、、
プログラミングにおける"run"は「実行」の意味で使われることが多いですよね。
"reduce"にもいろんな意味があります。
先入観にとらわれずに学習を続けたいです。

最後まで読んでいただきありがとうございました!

Discussion