106
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

【TypeScript/JavaScript】配列操作reduce()を救いたい。

Last updated at Posted at 2023-06-21

Array.prototypeのreduce()メソッド。
苦手な方や、ややこしいから使わないようにしているという方もいるかな、と。
確かに、他のmap()filter()sort()などと比べるとちょっと難解な感じありますよね。

また、一方では、reduceマジ最強と思ってる方もいると思います。
確かに、集計することも新たな配列を作ることも何でもできますもんね。

そんな避けられたり奉られたりなreduce()について、本当はどんなメソッドなのか自分なりの解釈を書きたいと思います。

(追記:ありがたいことに色々と有益なコメントを頂いているのでそちらも是非ご参照ください。)

reduce()とは

reduce() メソッドは、配列のそれぞれの要素に対して、ユーザーが提供した「縮小」コールバック関数を呼び出します。その際、直前の要素における計算結果の返値を渡します。配列のすべての要素に対して縮小関数を実行した結果が単一の値が最終結果になります。

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

reduce() で一番わかりやすいのは、配列のすべての要素の和を返す場合でしょう。

MDN Web Docs

Array.prototypeのメソッド内で唯一"試してみましょう"の上に3段落記述があります。
説明もとっつきにくく、"ユーザーが提供した「縮小」コールバック関数"とかなにそれ感ありますね。
あまつさえ、「一番わかりやすいのは」と言う導入部が難解な雰囲気と共に実は何でもできるけどね感を出しています。

可哀想ですね。救いましょう。

英単語としてのreduce

さて、いきなりですが、"reduce"という単語からどのような意味を連想しますでしょうか?
おそらく多くの方が「減らす」を思い浮かべるかと思います。
実際Googleで検索しようとすると「減らす」って出てきますしね。
難解さの一因はここにあると思います。
image.png

「減らす」で間違ってはいないはずですが、「配列を減らすってなんかイメージできん。。。filter()っぽくなっちゃわないか。。。」となりますよね。
あまつさえ、MDNのサンプルコードは要素の和を求めるものです。「減らす」なのに和を求めるって?え?となって当然です。

英英辞書Oxfordで調べてみましょう。下の方にちょっとだけ書いてありました。

reduce something to something | reduce to something
to change something to a more general or more simple form

(日本語訳:何かをより一般的な、あるいは、より簡単な形に変えること)

Oxford - reduce

「減らす」以外にも意味があるようです。ChatGPTにも聞いてみましょう。

英単語「reduce」は、何かを減らす、縮小する、または簡略化するという意味を持ちます。

image.png

もう一度ChatGPTに。(ちょっと回答を誘導する聞き方をしてるのはご容赦ください🙇‍♂️)

配列操作のreduce関数では、要素を処理しながら「複雑なデータを単純化する」「要素を結合して単一の値を生成する」という意味での「簡単にする」が近いイメージです。つまり、配列を簡潔な形にまとめるために使用されることが多いです。

image.png

reduce()は、配列を、「減らす」と言うより、「簡単にする」メソッドとして捉えると良さそうですね。

少し余談っぽくなりますが、
仕事で大量のデータをスプレッドシートに貼り付けたら上司からもう少し見やすくしてくれ、って言われることありますよね。
余分な情報を削って資料を簡潔にする(減らす→簡単にする)、これがイメージに近いかなと。

要素の和を求める、というのも、商品たくさん売れたらしいけど結局合計いくら売れたの?、という感覚で理解するとピンと来るのではないでしょうか。

引数が多いreduce

array.reduce((previousValue, currentValue, currentIndex, array) => {
  /* 処理 */
}, initialValue)

大枠から見ると、reduceの引数は、コールバック関数と、初期値のinitialValue、の二つですが、
コールバック関数内の引数が4つとあいまって見通し悪いですよね。
難解さのもう一つの要因はこれかなと。

でも、引数ほとんど省略可能なんですよね。
第二引数のinitialValue、コールバック内のcurrentIndexarray、これらは省略可能です。

実際みなさんにお聞きしたいところではあるのですが、特に、currentIndexarray、使いますか?
私はほとんど利用しません。

なので、ほとんど下記の形です。

array.reduce((previousValue, currentValue) => { /* 処理 */ }, initialValue)

ついでに、previousValueとかcurrentValueとか、長ったらしいので、短くして下記の形です。
「簡単にする」メソッドなのだから記述も簡単にしましょう。

array.reduce((p, c) => { /* 処理 */ }, initialValue)

初期値もなく配列の要素だけでいいのならばinitialValueも省略できます。

array.reduce((p, c) => { /* 処理 */ })

これぐらいならだいぶ見通しいいんじゃないでしょうか。
改修のたびに引数が4個5個6個と肥大化していくファットなクラスよりもはるかにシンプルですよね。

補足
コールバック関数内の引数の意味は下記のとおりです。
previousValue:前回コールバック関数の結果
currentValue:順番が回ってきたこれから処理する要素
currentIndex:現在のインデックス=currentValueのインデックス
array:元の配列

追記

頂いたコメントからの追記です。

英語版でのMDNでは、
previousValueではなく、accumulatorと表記がされています。

accumulatorは、「累積値, 累積するもの、蓄積するもの」と言った意味を持ちます。

また、省略形の記述もそれに倣って、下記の記述がよく見られます。

よく見られる使用例
array1.reduce((acc, cur) => { /* 処理 */ }

accumulatorpreviousValue、どちらで理解していただいてもいいと思います。
プロジェクト内でどのように利用されているかを見てみるのもいいと思います。

"「縮小」コールバック関数" とは

「1 + 2は?」「3」
「そこに3足すと?」「6」
「さらに4足すと?」「10」
と言う会話をイメージしてください。
この会話をソースコードに当てはめると下記のようになります。

和を求める
const ary = [1, 2, 3, 4];
const sum = ary.reduce((p, c) => p + c);
console.log(sum); // 10

この(p, c) => p + c部分が、"「縮小」コールバック関数"、配列を簡単にしていく処理です。
処理の流れを会話と照らし合わせます。
「1 + 2は?」「3」 → (1, 2) => 1 + 2
「そこに3足すと?」「6」 → (3, 3) => 3 + 3
「さらに4足すと?」「10」 → (6, 4) => 6 + 4

どうですか?難しくはないでしょう?

何でもできるreduce

さて、今度は逆に何でもできるが故の悩みですね。

冒頭で書いた通り、reduce()は、集計することも、新たな配列を作ることも、フィルタリングすることも、ソートすることも、最小値/最大値だけを取り出すことも、できますよね。

まあ!何でもできるなんて!優秀な子!
と言いたい気持ちも分かりますが、適材適所を忘れてはいけません
reduce()は単一の結果を導く「簡単にする」メソッドです。

何でもできるreduce
const users = [
    { id: 1, rank: 1 },
    { id: 2, rank: 10 },
    { id: 3, rank: 50 },
    { id: 4, rank: 100 },
];

// reduceを使った場合
const highRankUserIds = users.reduce<number[]>((p, c) => {
    if (c.rank >= 50) p.push(c.id);
    return p;
}, []);

// reduceを使わない場合
const highRankUserIds = users.filter(u => u.rank >= 50)
  .map(u => u.id);

見比べていただくとreduce()を使わない方が記述量も少なく理解もしやすいと思います。
条件で絞り込むならfilter()、新しい配列にするならmap()
きっとreduceも「これ俺がやるの・・・?」と思ってることでしょう。
適材適所です。

補足
reduce<T>()
TypeScriptの場合、reduceはジェネリクスで最終結果の型を指定することができます。
指定しない場合はinitialValueの型が、initialValueを省略した場合は元の配列の型が推論されます。

追記

頂いたコメントからの追記です。

先程の例で、reduce()からfilter()map()へ分解を行いましたが、この時ループ処理も1回から2回に増えています。
配列の量が膨大な場合は、reduce()で一度に処理をしてしまった方が効率的な可能性があります。
パフォーマンスの面からも適材適所の選択ができると良さそうです。

結局いつ使うのreduce

やっぱり集計とか何かしら単一の結果に導くものが適していると思います。
最もシンプルな例、初期値を用いた例、indexを用いた例をそれぞれ書いていきます。

購入商品の値段の合計を求める(最もシンプルな例)
const items = [
    { id: 1, price: 100, amount: 20 },
    { id: 2, price: 1000, amount: 2 },
    { id: 3, price: 350, amount: 1 },
];
const sum = items
  .map(item => item.price * item.amount)
  .reduce((p, c) => p + c);
console.log(sum); // 4350
配列からRecordに変換(初期値を用いた例)
const languages = [
    { jp: '日本語', en: 'Japanese' },
    { jp: '英語', en: 'English' },
    { jp: '中国語', en: 'Chinese' },
];
const dictionary = languages.reduce<Record<string, string>>((p, c) => {
    p[c.jp] = c.en
    return p;
}, {})
console.log(dictionary); // { "日本語": "Japanese", "英語": "English", "中国語": "Chinese" } 

余談
reduce()のコールバック関数内はできるだけシンプルに保つと見通しを壊さないと思います。
全部を一気にやろうとせず、reduce()の前に、
整形やフィルターは前処理としてmap()filter()などで済ませると吉です。

ちなみに、上記「配列からRecordに変換」の例をmap()でやろうとすると、近しい処理はできますが、
単一の結果にならず惜しい感じになります。
map()は新しい配列にする処理。reduce()は単一の結果にする処理。

NG:配列からRecordに変換(mapだと上手くいかない)
const languages = [
    { jp: '日本語', en: 'Japanese' },
    { jp: '英語', en: 'English' },
    { jp: '中国語', en: 'Chinese' },
];
const dictionary = languages.map(lang => ({ [lang.jp]: lang.en }))
console.log(dictionary); // [{ "日本語": "Japanese" }, { "英語": "English" }, { "中国語": "Chinese" }] 

もう一個。ほとんど使わないと言ったcurrentIndexを用いた例。

配列を任意のサイズ毎に区切る(currentIndexを使った例)
const eachSlice = (ary: number[], size: number): number[][] => ary.reduce<number[][]>(
    (newArray, _, i) => (i % size ? newArray : [...newArray, ary.slice(i, i + size)]),
    [],
  );

const ary = [1, 2, 3, 4, 5, 6, 7, 8]
console.log(eachSlice(ary, 2)); // [[1, 2], [3, 4], [5, 6], [7, 8]] 
console.log(eachSlice(ary, 4)); // [[1, 2, 3, 4], [5, 6, 7, 8]] 

ここまで来るとちょっとうーんってなりますね。。

最後に

reduce()がとっても好きなので解説記事を書くつもりがこんな記事になってしまいました。
少しでもreduce()の用法・用量が皆様に伝われば幸いです。

また、reduce()へのアツい想いやreduce()を救うアイディア・考え方など、ございましたら遠慮なくコメントくださいませ。

106
61
14

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
106
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?