💊

【Node】関数型プログラミングについて | reduceの基礎と実践

2023/01/25に公開
2

この記事は?

JavaScriptのreduceは配列操作において有用な関数だが、慣れないとなかなか取っ付きにくい。配列を操作するためにmapやfilterなど駆使しなくてもreduceだけで解決することもあり、活用できれば便利な関数と言える。本記事ではその基礎と現場で使えるように具体例まで解説する。

対象読者

・中級以上を目指すJavaScriptプログラマー
・reduceについてよくわからないけど知りたい人

map/filter

reduceに入る前に、配列操作でよく使うJavaScriptのビルドイン関数であるmapとfilterを見ていきます。よく使うので、ドキュメントを見ずにすぐに引き出せるくらいが望ましいでしょう。

map
配列から一つ一つ値を取り出して、その値に由来する新しい配列を返す。
□□□□.map(□ -> ●) -> ●●●●
[0, 1].map(val => val + 2) // => [3, 4]

filter
配列の中身を一つ一つ精査して、条件に合う要素だけからなる配列を返す。
□□■□.filter(□) -> □□□
[0, 1, 1].filter(val => val === 1) // => [1, 1]

一方、reduceはもう少し複雑な配列操作において有用なことがあるので、次に見ていきます。

reduce

reduce
reduceを理解する上でポイントとなると考えられるのは、以下の4点です。
・reduceでは、各要素に対して処理を行うコールバック関数fnを与える。
・fnが要素ごとに計算を行う。計算結果は次の要素の処理に持ち越され使われる。
・初めの計算では前要素がなく計算結果がないので、第二引数の初期値が使われる。
・要素要素で計算が行われた後、最終要素での計算結果がreduceの返却値となる。

■■■■.reduce((previousValue, currentValue, currentIndex, array) => {fn, initialValue})
previousValue: 直前要素での関数fnの処理結果
currentValue: その配列での関数fnの処理結果
currentIndex: 処理を行なっている要素のインデックス
array: reduceに与えている配列
fn: reduceに与えているコールバック関数
initialValue: 1つ目の要素計算で使われる値

例えば、シンプルな例として配列の要素を足し上げる実装は以下の通り実装できます。

const sum = [0, 1, 2, 3].reduce(function (previousValue, currentValue) {
  return previousValue + currentValue
}, 0) // => 6
*アルゴリズム
計算1回目: 0(initialValue) + 0(currentValue) = 0 
計算2回目: 0(previousValue) + 1(currentValue) = 1
計算3回目: 1(previousValue) + 2(currentValue) = 3
計算4回目: 3(previousValue) + 3(currentValue) = 6 // reduce関数として返す返り値

ポイントは、それぞれの配列の要素でコールバック関数による計算を行い、その要素の計算結果を次の要素での計算に回し回し計算し、最終要素での計算結果が返り値になること。

今見たような例は簡単で、現場ではもっと複雑な配列操作に役立つことがあるので、次に現場寄りの実践的なサンプル実装を2つ見ることでreduceをどう使うのか?考えましょう。

例1)要素の拾い上げ

[{ a: 0 }, { b: 2 }, { a: -3 }, { a: 3, b: 2 }] という配列に対して、
配列内にある各キーa, bが持っている数値をキーごとに足し合わせるとa=0, b=4になる。
[{ a: 0 }, { b: 2 }, { a: -3 }, { a: 3, b: 2 }] に対して {a: 0, b: 4} を返す関数を作成せよ。

Answer.

interface Sample {
  foo?: number;
  bar?: number;
}

const sampleArray: Sample[] = [
  { a: 0 },
  { b: 2 },
  { a: -3 },
  { a: 3, b: 2 },
];

const sumObj: Sample = {
    a: 0,
    b: 0,
};
const newArr: Sample[] = sampleArray.reduce((previous: Sample, current: Sample) => {
    return {
        a: previous.a + (current?.a || 0),
        b: previous.b + (current?.b || 0),
    };
}, sumObj); // => {a: 0, b: 4}

Discussion.

ここでやっていることとしては、
①初期値として0の値を持つa,bのオブジェクトsumObjを与え
②各要素のreturnでもa,bのキー/値をオブジェクトとして返すことで
③計算が最終行にたどり着いたときにそれまでの蓄積の計算結果をreduceの結果として返す。

filterやmapでも同じことができるのではないか?という問いに対しては、
reduceを使わないと、filterやmapを何回も使うことになったり、やたらとコードが長くなったりしてしまうが、ここでは一回reduceを使えば十分なためreduceを用いています。

例2)配列の作り直し

以下のInputに対応したOutputを返す関数を作成してください。

Input: 
const input = [
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-15",
        samples: [
            { nameTitle: "sample1", unit: "dollar" },
            { nameTitle: "sample1-2", unit: "dollar" },
        ],
    },
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-16",
        samples: [{ nameTitle: "sample2", unit: "yen" }],
    },
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-17",
        samples: [{ nameTitle: "sample2", unit: "peso" }],
    },
];

Output: 
[
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-15",
        index: 0,
        sample: { nameTitle: "sample1", unit: "dollar" },
    },
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-15",
        index: 1,
        sample: { nameTitle: "sample1-2", unit: "dollar" },
    },
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-16",
        index: 2,
        samples: { nameTitle: "sample2", unit: "yen" },
    },
    {
        dateFrom: "2022-11-12",
        dateTo: "2022-11-17",
        index: 3,
        samples: { nameTitle: "sample2", unit: "peso" },
    },
];

Answer.

const getResult = (input) => {
  let count = 0;
  const result = input.reduce((previous, current) => {
    const rows = current.samples.map((sample) => {
      return {
        dataFrom: `${current.dateFrom}`,
        dataTo: `${current.dateTo}`,
        index: count++,
        sample: sample,
      };
    });
    return previous.concat(...rows);
  }, []);
  return result;
};

Discussion.

① 初期値として空配列を与える
② reduceで一つ一つ要素を処理するが、samplesはmap関数でバラす
③ samples配列は1つ1つのsampleにバラされる
④ バラしたsampleを含むオブジェクトをmapで返す(□□□□.map -> ■■■■)
⑤ mapで返った配列をreduceのスコープでの1要素計算結果として返す
⑥ ②~⑤を繰り返し行うことで最終計算値としてreduceの返り値を得る

concatは、配列に新たに要素を追加するビルドイン関数である。

最後に

reduceについては公式Docに以下の記載がある。

reduce() のような再帰的な関数は強力ですが、
特に経験の浅い JavaScript 開発者にとっては理解するのが難しい場合があリます。

また、公式Docの言葉を借りると、reduce() は関数型プログラミングの中心的な概念である。いずれにせよreduceは慣れないと取っ付きにくい関数には見えるが、使えこなせれば配列を強力に操作する関数にもなるので、使えるようになっておくべきだと考え本記事で解説した。

参考文献

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

Discussion

akiosmakiosm

例2 の index の値が期待する出力結果にならないようです。

rio_devrio_dev

修正いたしました。
教えていただきありがとうございます。