🐝

【TS】今さら聞けないreduce

2020/11/04に公開

はじめに

JavascriptもしくはTypescriptで配列を操作する際に、reduceを使ったことがありますか?
「コアな部分で使っているのは見たことあるけど、どんな関数なのかはイマイチ分からない」という方もいるのではないでしょうか。

私自身の話になりますが、ReactReactNativeの開発プロジェクトをいくつか経験したことがあります。
たまに新入社員の方がアサインすることもあり「まずはJSTSを知ろう」といったレベルから教えることもあります。
皆さん配列に関しては最初「forforEachをグルグル回して処理する」というアプローチを取り、そこから段々とmapfilterなどを用途に合わせて覚えていきますが、大概reduceでつまづきます。

なぜなんだろう?と考えた時「filtersomeに比べてreduceという名前が直感的でない」や「reduceは何でもできてしまうが故に取っ付きにくい」など色々と理由が浮かびます。
要するに 「何をやっているのか分かりにくい」 のだと思います。

そこで今回はTypescriptのサンプルを見ながら、配列界随一の万能関数reduceについて解説していきたいと思います。

その前に・・・

reduceについて解説する前に、その他の有名どころの関数について紹介したいと思います。
基本となるforEachについては割愛したいと思います。

また、以降のソースについては基本的には以下のようなStudentという型と、そのインスタンス3件のデータが入った配列を用いていくこととします。
namescoreというプロパティを持つオブジェクト型になります。

type Student = {
    name: string;
    score: number;
}

const data: Student[] = [
    { name: '太郎', score: 75 },
    { name: '花子', score: 62 },
    { name: 'John', score: 59 }
]

なお、紹介する各関数について全ての引数を扱うわけではありません(例えば、コールバック関数の実行時にthisをバインドするために引数thisArgs等がありますが、ここでは割愛しています)。

新しい配列を作るmap

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

mapは元の配列から 「新しい配列」 を作ります。
mapの第一引数で渡した関数内のreturnした値がまとまって配列になります(何もreturnしない場合はundefiendが入ります)。

const scoreList: number[] = data.map((d: Student): number => {
    return d.score;
});

console.log(scoreList); // --> [75, 62, 59]

特定の要素を取得するfilter,find

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

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

どちらも第一引数で渡した関数内で真を返した配列要素だけを抽出します。
filterは配列、findは単一要素を返します。

const under65List: Student[] = data.filter((d: Student): boolean => {
    return d.score < 65;
});

console.log(under65List); // --> [{ name: '花子', score: 62 }, { name: 'John', score: 59 }];

const taro: Student = data.find((d: Student): boolean => {
    return d.name === '太郎';
});

console.log(taro); // --> { name: '太郎', score: 75 }

配列そのものが目的条件に合致しているかを判定するsome,every

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

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

どちらも真偽値を返却します。
配列中の値に対して第一引数の関数を実行していき、someは「1件でも条件を満たしていたら」、everyは「全件条件を満たしていたら」trueを返します。

const someOver70: boolean = data.some((d: Student): boolean => {
    return d.score > 70;
});

console.log(someOver70); // --> true

const everyOver70: boolean = data.every((d: Student): boolean => {
    return d.score > 70;
});

console.log(everyOver70); // --> false

新しい単一要素を返すreduce

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

いよいよreduceについての説明です。
reduceは使い勝手としてはmapに近いかなと思います。

第一引数には最低2つの引数を取る関数を渡します。
この2つの引数はそれぞれ 「アキュムレーター」「現在の値」 と呼ばれています。

アキュムレータには、「初期値」もしくは「直前の処理で返された値」が入り、現在の値にはその名のとおり「今現在処理している配列要素」が入ります。

また、第二引数には「アキュムレータ」の初期値を指定します。

// 全員のscoreの合計を取得する
const totalScore: number = data.reduce((acc: number, val: Student): number => {
    // accは「初期値 or 前回のreturn値」でvalは「配列要素」
    return acc + val.score;
}, 0);

console.log(totalScore); // --> 196

reduceがなぜ万能と言われるかというと、今まで紹介した関数をreduceで置き換えることができるからだと思います。

例えばmapの例のようにscoreの値の配列を取得したい場合は下記のようにすることで実現できます。

const scoreList: number[] = data.reduce((acc: number[], val: Student): number[] => {
    return [...acc, val.score];
}, []);

console.log(scoreList); // --> [ 75, 62, 59 ]

同様にしてfilterfind,some,everyも下記のように置き換えることができます。

const under65List: Student[] = data.reduce((acc: Student[], val: Student): Student[] => {
    if(val.score < 65) return [...acc, val];
    return acc;
}, []);

console.log(under65List); // --> [{ name: '花子', score: 62 }, { name: 'John', score: 59 }];

const taro: Student | undefined = data.reduce((acc: Student | undefined, val: Student): Student | undefined => {
    if(acc) return acc;
    return val.name === '太郎'? val : undefined;
}, undefined);

console.log(taro); // --> { name: '太郎', score: 75 }

const someOver70: boolean = data.reduce((acc: boolean, val: Student): boolean => {
    if(acc) return acc;
    return val.score > 70;
}, false);

console.log(someOver70); // --> true

const everyOver70: boolean = data.reduce((acc: boolean, val: Student): boolean => {
    if(!acc) return acc;
    return val.score > 70;
}, true);

console.log(everyOver70); // --> false

初期値

第二引数の初期値は省略可能です。
その場合、アキュムレータの初期値には「配列の0番目の要素」が入り、現在の値は「配列の1番目の要素」から始まります。
ループの開始位置が変わるため、初期値を設定しない方が設定した場合に比べてループ回数が1減ります。

  • 初期値なし
ループ回数 acc val
1 0番目の要素 1番目の要素
2 1回目のループのreturn値 2番目の要素
  • 初期値あり
ループ回数 acc val
1 初期値 0番目の要素
2 1回目のループのreturn値 1番目の要素

まとめ

今回はreduceについて、Typescriptのコードベースで解説を行いました。
何でもできてしまうが故によく分からない、となりがちな関数ですが一度覚えて使いこなせば非常に役立つと思います。
※なお、今回はパフォーマンスについては言及せず、挙動についての解説に留めています。

この記事がreduceやその他関数の学習に役立てば幸いです。

Discussion