🥿

flatMapはfilterを兼ねることができるけど遅い

2023/11/19に公開
5

※頂いたコメントについて追記があります(2023/11/20)

最近、自社のコードレビューで次のような記述を目にしました。

const result = radomNumber.flatMap((item) => {
  if (item > 5) {
    return item * 2;
  }
  return [];
});

flatMapは空配列をflatにする際に省けるのでfilterの処理をついでに書けるという事です。つまり、次のコードと返却値は同じです。

const result = radomNumber
  .filter((item) => item > 5)
  .map((item) => item * 2);

flatMapの方が早くなる可能性があるのかな?(flat()の処理がループより早い可能性)と思いまして計測してみました。
次が計測用のコードです。


  const radomNumber = Array.from({ length: 10000000 }, () =>
    Math.floor(Math.random() * 10)
  );

  const f1 = () => {
    const start = performance.now();
    
    const result = radomNumber
      .filter((item) => item > 5)
      .map((item) => item * 2);

    const end = performance.now();
    console.log("f1 time", end - start);
  };

  const f2 = () => {
    const start = performance.now();
    
    const result = radomNumber.flatMap((item) => {
      if (item > 5) {
        return item * 2;
      }
      return [];
    });

    const end = performance.now();
    console.log("f2 time", end - start);
  };

ブラウザ上でそのまま実行しました。
結果は

f1 time 177
f2 time 520

よく考えたらflatMapは配列の除去がないため、計算量が常にflatMap≧filter&mapとなり遅くなることに気づきました。
※flatMapは常に2N回、filter&mapは最小でN回。

flatMapのユースケース(追記: 2023/11/20)

コメントで頂いた指摘の通り、typescriptだと配列にnull,undefinedを含む場合、is possibly 'undefined'となるため、filter&mapでそのまま書けないという問題があります(型アサーションする必要がある)。
こういう時はflatMapを使うのはアリだと思いました
(昔、この場面に出くわしたときは元配列の作られ方の見直しで回避した記憶があります)。

ちなみに遅いといっても計測の通り、明確な遅延は実行環境によるといえど、数100万件とか超えるあたりで発生するのでWebアプリでこの点を考慮する必要はほとんどないと思います(ユーザーのリクエスト&レスポンスにその件数の処理が走ることは避けるべき)。

結論

可読性の問題もあるので、普通にfilter&mapで書くべきだなと思いました。
可読性の問題はあるので、nullishな問題以外でflatMapでfilterを兼ねるのはよそうと思いました。

Discussion

The WorldThe World

小ネタですが、FlatMapundefinednullを除去できるといったこともできます。
filterを使った場合asとかで型を変更することはできますが、コードが間違っていた場合エラーを起こすことがあるのでflatMapが役に立ちます。
Type Guardを使えばfilterでも同じことができますが、配列に複数の型がある場合((string | number | undefined)[])はflatMapだと簡潔に書けます。
他の方法
どちらにせよ、TSで書くならこの文脈でflatMapは使わなくて良さそうですね。ですが、JSで書く際はエディター(VS Code等)の補完が欲しい時はもしかしたらflatMapが役に立つかもしれませんね。

const array: (number | undefined)[] = [1, 2, 3, 4, undefined, 5, undefined];

// (number | undefined)[] 
const filteredArray = array.filter(v => !!v);

// number[]
const flatMapArray = array.flatMap(v => v ? [v] : []);

TS Playground

mpywmpyw

ちょっと気になったんですが、配列の生成コストってかかってないでしょうか? VM が最適化してくれてるかもしれませんが…

もし配列リテラルを外出しして使い回すとどうなるのか気になります

jlmn1026jlmn1026

こういうことですよね?
10回くらい見た感じわずかに(1,2%)早くなってはいました。

const emptyArray = [];
const result = radomNumber.flatMap((item) => {
    if (item > 5) {
      return item * 2;
    }
    // return [];
    return emptyArray;
});

mpywmpyw

支配的ではなさそうですね!ありがとうございます