👌

flatMapという概念を理解してみる

2024/08/20に公開

JSのArrayメソッドでよく見かけるflatMapですが、正直よくわかりません。
実際に使われているコードを見ても、結局flatMapの結果がどのデータ型で返ってくるかなどを読み解けませんでした。
よって、まとめてみながら徐々に理解していきます。

公式ドキュメント

flatMap

flatMap() は Array インスタンスのメソッドで、最初にマッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内に平坦化します。これは、 map() の後に深さ 1 の flat() を行うのと同じですが (arr.map(...args).flat())、これら 2 つのメソッドを別々に呼び出すよりもわずかに効率的です

ドキュメントには上記のように書いてありますが、何を言ってるかわかりません。
要はmap()の後にflat()を行っているようなのですが、「平坦化」という概念が特にわかりませんし、「わずかに効率的」って、それ使う意味あるのか???という気持ちです。

そこでflatのドキュメントを見ると以下です。

flat

flat() は Array インスタンスのメソッドで、すべてのサブ配列の要素を指定した深さで再帰的に結合した新しい配列を生成します。

flatに関しては、もはや何も言ってないのと同じくらいよくわかりません。

これをマシな理解にしていこうと思ってます。

頑張って理解

まずはflat

なんとなく日本語にした方が理解しやすいと思ったので、
ドキュメントにある例を、国、都道府県、市区町村、番地という階層に置き換えて考えてみます。

// ドキュメントの例
const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

// 置き換え
const arr2 = [アメリカ, イギリス, [東京, 大阪, [横浜, 名古屋]]];
arr2.flat();
// [アメリカ, イギリス, 東京, 大阪, [横浜, 名古屋]]

むしろわかりにくくなった気もします。

最上位階層が「国」の配列に対してflat()を効かせると、
2階層目にあった「都道府県」要素が、1階層目の「国」と同じ階層に上がってくるようです。

単に一つ下の階層を分解するイメージですね。
置き換えない方が良かったです。

flatMapは?

flatMapを使いたい時は、ネストされた階層の奥の要素に何か処理を行いたい時です。
よってもう少し処理自体を複雑にし、以下で考えてみます。

  • お題:人口50万人以上の市区町村を出力せよ
  • 条件:以下regionsというオブジェクトが与えられる
const regions = [
  {
    prefecture: '東京',
    cities: [
      { city: '新宿区', population: 339000 },
      { city: '渋谷区', population: 227000 },
      { city: '世田谷区', population: 939000 }
    ]
  },
  {
    prefecture: '大阪',
    cities: [
      { city: '大阪市', population: 2750000 },
      { city: '堺市', population: 830000 },
      { city: '東大阪市', population: 500000 }
    ]
  },
  {
    prefecture: '岐阜',
    cities: [
      { city: '岐阜市', population: 400000 },
      { city: '大垣市', population: 160000 },
      { city: '高山市', population: 90000 }
    ]
  }
];

map()だけで処理した場合

const largeCities = regions.map(region => 
  region.cities.filter(city => city.population >= 500000)
);

console.log(largeCities);

map()だけで処理を行った場合、以下のようにネストされた配列が返り値になってしまいます。

[
  [
    { city: '世田谷区', population: 939000 }
  ],
  [
    { city: '大阪市', population: 2750000 },
    { city: '堺市', population: 830000 },
    { city: '東大阪市', population: 500000 }
  ],
  []//岐阜の処理結果
]

これではTypeScriptなどの静的型付け言語の場合、型エラーを起こしてしまいがちですね。
上記を見て、flatMapの力は理解できた気がしますが、一応ステップを踏んでみます。

map()の後にflat()を実行する場合

const largeCities = regions.map(region => 
  region.cities.filter(city => city.population >= 500000)
).flat();

console.log(largeCities);

このようにmap()化した処理の後でflat()を行うと、ネストされた配列を取り出して以下のような実行結果になります。

[
  { city: '世田谷区', population: 939000 },
  { city: '大阪市', population: 2750000 },
  { city: '堺市', population: 830000 },
  { city: '東大阪市', population: 500000 }
]

得たい結果は得られましたが、「市区町村の配列が欲しい」などの際に毎回flat()を書くのは面倒ですね。
そこで登場したのがflatMap()のようです。

flatMap()を使った場合

const largeCities = regions.flatMap(region => 
  region.cities.filter(city => city.population >= 500000)
);

console.log(largeCities);

実行結果はmap()の後にflat()を行った場合と同じく、以下になります。

[
  { city: '世田谷区', population: 939000 },
  { city: '大阪市', population: 2750000 },
  { city: '堺市', population: 830000 },
  { city: '東大阪市', population: 500000 }
]

なるほど、これを把握した上でドキュメントを見返すと、理解できますね。

flatMap() は Array インスタンスのメソッドで、最初にマッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内に平坦化します。これは、 map() の後に深さ 1 の flat() を行うのと同じですが (arr.map(...args).flat())、これら 2 つのメソッドを別々に呼び出すよりもわずかに効率的です

おっしゃる通り、という感じです。確かに、「わずかに効率的」ですね。

TypeScriptなどの静的型付け言語で書く場合は、返り値の型も重要になってきます。
ネストされた階層を持つオブジェクトに対して処理を行い、その配列をばらけさせたい場合は、このようにflatMap()を行うとスムーズですね。

補足

flatMap()では平坦化の深さを指定することはできませんが、
より奥の階層まで平坦化したい場合は、flat()を使い、以下のように指定できるようです。

const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

これもいつか使う日が来るかもしれないので、引き出しに入れておきます。

Discussion