7
1

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.

NIJIBOXAdvent Calendar 2023

Day 6

Array.prototype.reduceはなぜ配列処理において万能なのか

Last updated at Posted at 2023-12-05

皆さんはArray.prototype.reduceを使ったことがあるでしょうか。
よくredcueはその使い方の例として下記のように配列に格納された数値を全部足した結果を返却する処理が示されます。

// 与えられた配列の値をすべて足した値を返却する例
arr.reduce((acc, current) => acc + current, 0);

しかし、reduceというメソッドは非常に柔軟で汎用的な配列操作が行えるメソッドでもあります。

reduceを利用して他のメソッドを実装してみる

reduceの汎用性を確かめるために、他の配列処理をreduceを使って再現してみようと思います。
文章量の限界もあるので全てのメソッドを記述することは出来ないため一部だけの紹介ですが、大抵の処理はreduceを利用することで記述可能です。

map

Array.prototype.mapは対象となる配列全てに関数を適用し、新しい配列を返す関数です。

この例ではnumber[]である配列arrの各要素を+1した配列を新しく生成します。

// mapでの処理
arr.map((v) => v + 1);
// reduceを使った処理
arr.reduce((acc, current) => {
  const newValue = current + 1;
  return [...acc, newValue];
}, []);

find

Array.prototype.findはコールバック関数を評価して、最初にtrueを返した値自身を返す関数です。

この例ではnumber[]の中から10以上である値を返す処理を記述します。

// findでの処理
arr.find((v) => v > 10);
// reduceを使った処理
arr.reduce((acc, current) => {
  if(acc !== undefined) {
    return acc;
  };

  return current > 10 ? current : undefined;
}, undefined);

toReversed

Array.prototype.toReversedは受け取った配列の順番を反転させた配列を返すメソッドです。

この例では、unknown[]である配列arrを反転させた配列を返す処理を記述します。

// toReversedでの処理
arr.toReversed();
// reduceを使った処理
arr.reduce((acc, current) => [current, ...acc], []);

reduceの汎用性

基本的には専用のメソッドを使ったほうが明示的かつ簡潔に処理を記述できるため、実務でこのような書き換えを行うことは殆どないかと思います。
しかしながら、reduceというメソッドの汎用性はなんとなく分かる内容になったかと思います。

いつ使うのか

基本的には専用のメソッドを利用したほうがいいと思いますが、複数回処理をチェーンして続ける場合にreduceの方が簡潔に記述できる場合があります。
例として記事データが格納されているPosts型を下記のように処理するコードを書く場合を例にそれぞれのコードの書き方を比較してみましょう。

  1. currentUserIduserIdが一致するものだけを抽出
  2. publishDatelastModifiedはDate型に変換
  3. publishDateを基準に並べ替えを行う
// 共通の型定義
type Posts = {
  userId: string,
  title: string,
  publishDate: string,
  lastModified?: string,
}[];
// reduceを使わない例
const currentUserId = 'foo';
const result = posts // Post[]型
  // `currentUserId`と`userId`が一致するものだけを抽出
  .filter((post) => post.userId === currentUserId)
  // `publishDate`と`lastModified`はDate型に変換
  .map((post) => {
    return {
      ...post,
      publishDate: new Date(post.publishDate),
      lastModified: post.lastModified ? new Date(post.lastModified) : undefind,
    };
  })
  // `publishDate`を基準に並べ替えを行う
  .toSorted((prev, next) => {
    return prev.publishDate.getTime() - next.publishDate.getTime();
  });
// reduceを使う例
const currentUserId = 'foo';
const result = posts // Post[]型
  .reduce((acc, current) => {
    // 必要な値の準備
    const publishDate = new Date(current.publishDate);
    const lastModified = current?.lastModified ? new Date(current.lastModified) : undefined;
    const isOld = (acc?.publishDate.getTime() ?? 0) > current.publishDate.getTime();

    // 返却する値の(構造)定義
    if (current.userId === currentUserId) {
      return acc;
    };

    return isOld ?
      [...acc, {...current, publishDate, lastModified}]:
      [{...current, publishDate, lastModified}, ...acc];
  }, []);

上記のコードは動かないんですが、なんとなくやりたいこととそれぞれのコードの書き方で表現した場合にどのようなコードになるのかという目安だと思ってください。

reduceを使わない例はそれぞれの処理が明確に別れていて、データを抽出して型を変換して、ソートするという流れが追いやすいコードになっています。
reduceを使ったコードの場合、はじめに必要な値を作成してその値をどのような構造で返すかという記述になっています。

個人的には前者のコードは仕様の変更が入った場合に、それぞれの処理の中で値がどのように変化しているかを意識する必要があるのに対して、後者に関しては一つの関数スコープの中で記述が収まっているため仕様の変更にも対応しやすいコードになっていると感じます。

まとめ

普段あまりreduceを使わない人も、汎用性の高いメソッドであることを理解して使えるようになっておくとよりよいコードが書けるようになるかもしれません。

7
1
0

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
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?