flatMapとはなんなのか 〜30代疲れ目エンジニアの平坦化考察〜

JavaScript や TypeScript で`map`はよく使うけど、`flatMap`って何者？そんな疑問、抱えてない？

「`map`は知ってるけど、`flatMap`って何が違うの？使うとどうなるの？」って思うよね。

今日はそんな、なんとなくわかってるようなわかってないような、
「いい加減そろそろちゃんと `flatMap` わかっときたいなー」って思ってる30代の疲れ目エンジニアのための話。

ちなみに「`map` って単なるループでしょ？」って思ってる人にとっても、意味のある話かもしれません。

`map`と`flatMap`の違いをゆる〜く考えて、`flatMap`の真の力を探ってみましょう。

---

## 1. まずはおなじみの `map` をおさらい

`map`は配列の各要素に関数を適用して、新しい配列を作ってくれるメソッドだよ。めっちゃ便利だよね。

```typescript
const numbers = [1, 2, 3]
const doubled = numbers.map((x) => x * 2)
console.log(doubled) // 出力: [2, 4, 6]
```

各要素を 2 倍にして、新しい配列`[2, 4, 6]`が返ってくる。
まぁシンプルでわかりやすいよね！

---

## 2. `flatMap` って何が違うの？

`flatMap`は、一言で言うと「`map`してから平坦化（フラットに）する」メソッド。
うん、それだけじゃピンとこないよね。
実際に見てみよう。

### 例 1: 要素を増やす

```typescript
const numbers = [1, 2, 3]
const result = numbers.flatMap((x) => [x, x * 2])
console.log(result) // 出力: [1, 2, 2, 4, 3, 6]
```

各要素を`[x, x * 2]`という配列に変換。`flatMap`はその結果をフラットにしてくれるので、最終的な配列は`[1, 2, 2, 4, 3, 6]`になるよ。

### 例 2: 要素を条件でフィルタリング

例えば、奇数のときだけ 2 倍の値も追加し、偶数は除外してみる。

```typescript
const numbers = [1, 2, 3, 4]
const result = numbers.flatMap((x) => (x % 2 !== 0 ? [x, x * 2] : []))
console.log(result) // 出力: [1, 2, 3, 6]
```

偶数の`2`と`4`はスキップされ、奇数の`1`と`3`は元の値とその 2 倍の値が追加されたね。

**つまり、`flatMap`を使うと「要素を増やしたり減らしたり」自由自在なんだよね！**

---

## 3. flatMap の本当の力を見てみよう

flatMapの真髄は、要素の増減じゃないんです。
以下のような処理を例にflatMapができることを見ていきましょう。

### 例題
1. 数字の1からスタート
1. 1を足す
1. 2倍する
1. 3倍する
1. 4倍する
1. 各計算ごとにログを出力する
1. 文字列に変換して!を付ける

### 手続き型のコードとの比較
まずは普通の手続き型で書いてみる。

```typescript
let x = 1
x = x + 1
console.log(`after add 1: ${x}`)
x = x * 2
console.log(`after mul 2: ${x}`)
x = x * 3
console.log(`after mul 3: ${x}`)
x = x * 4
console.log(`after mul 4: ${x}`)
x = `${x}!`
console.log(`Final Result: ${x}`)

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

変数`x`を順番に更新していくスタイル。
これは**文の連続**だね。

---

## 4. `flatMap`で同じことをやってみる

`flatMap`を使って、一連の処理を繋げてみよう。

```typescript
const processFlow = [1]
  .flatMap((x) => [x + 1])
  .flatMap((x) => (console.log(`after add 1: ${x}`), [x]))
  .flatMap((x) => [x * 2])
  .flatMap((x) => (console.log(`after mul 2: ${x}`), [x]))
  .flatMap((x) => [x * 3])
  .flatMap((x) => (console.log(`after mul 3: ${x}`), [x]))
  .flatMap((x) => [x * 4])
  .flatMap((x) => (console.log(`after mul 4: ${x}`), [x]))
  .flatMap((x) => [`${x}!`])

console.log(`Final Result: ${processFlow[0]}`)

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

各ステップで計算結果をログ出力しながら、flatMapで次の処理に繋げることができるんだ。

最終的な値の結果は手続きと同様に `"48!"` になったね。

これで一連の処理が **一つの評価可能な式** としてまとまっているんだよね。

---

## 5. シンプルな箱（Box）を作ってみよう

配列の flatMap が何をやっているのかを紐解くために、配列と同じような構造を自分で実装して考えてみよう。

配列は複数の値を包む箱のようなもの。
同じように、**一つだけの値を包む箱**を自作してみよう。

### Box を定義する

まずは、値を保持してそれを返すだけのシンプルな **Box** を定義。

```typescript
type Box<T> = {
  getValue: () => T
}

const box = <T>(value: T): Box<T> => ({
  getValue: () => value,
})
```

これで、`box(1)`とすると、値`1`を持った Box ができるよ。

---

## 6. 箱の中の値を操作したい！

でも、このままだと値を取り出せるだけ。
値を箱からわざわざ取り出さずに、箱の中の値を直接操作したくなるよね？
そこで、「箱に入れた値を操作する」方法を考えよう。

### 「箱の中の値に対して関数を適用し、その結果を新しい箱に入れて返す関数」`map` を定義する

`map`を使って、箱の中の値を操作して新しい箱を返せるようにする。

```typescript
type Box<T> = {
  map: <U>(fn: (value: T) => U) => Box<U>
  getValue: () => T
}

const box = <T>(value: T): Box<T> => ({
  map: (fn) => box(fn(value)),
  getValue: () => value,
})
```

これで、箱の中の値に関数を適用して、新しい箱を作れる。

```typescript
const result = box(1)
  .map((x) => x + 1)
  .getValue()

console.log(result) // 出力: 2
```

---

## 7. `map`でも同じことはできないの？

さっきの連続した処理を`map`で書いてみよう。

```typescript
const result = box(1)
  .map((x) => x + 1)
  .map((x) => (console.log(`after add 1: ${x}`), x))
  .map((x) => x * 2)
  .map((x) => (console.log(`after mul 2: ${x}`), x))
  .map((x) => x * 3)
  .map((x) => (console.log(`after mul 3: ${x}`), x))
  .map((x) => x * 4)
  .map((x) => (console.log(`after mul 4: ${x}`), x))
  .map((x) => `${x}!`)
  .getValue()

console.log(`Final Result: ${result}`)

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

`map`でも同じように処理を連結できる。

でも、引数の関数が新たな箱（Box）を返す場合、`map`ではうまく連結できないんだ。

---

## 8. `flatMap` の必要性

例えば、箱（Box）を返す関数があるとする。

```typescript
const add = (y: number) => (x: number) => box(x + y)
```

この`add`関数は、値を受け取って新しい Box を返す。

### `map`で試してみると…

この`add`関数を2回適用してみる

```typescript
const result = box(1)
  .map(add(1))
  .map(add(1))
  .getValue()

console.log(result) // 出力: Box { map: [Function], getValue: [Function] }
```

あれ？結果が Box の中に Box が入ってしまった。
`map`を使うと、ネストした箱になってうまく処理が連結できなくなってしまう。
無理やり値を取り出そうとすると以下のようになる。


```typescript
const result = box(1)
  .map(add(1))
  .getValue()
  .map(add(1))
  .getValue()
  .getValue()

console.log(result) // 出力: 3
```

このように`map` だとネストした箱の処理の連結させるのはめんどくさい。



### 「箱の中の値に対して箱を返す関数を適用し、その結果の箱を返す関数」`flatMap` を定義する

この問題を解決するために、`flatMap`を定義する。

```typescript
type Box<T> = {
  map: <U>(fn: (value: T) => U) => Box<U>
  flatMap: <U>(fn: (value: T) => Box<U>) => Box<U>
  getValue: () => T
}

const box = <T>(value: T): Box<T> => ({
  map: (fn) => box(fn(value)),
  flatMap: (fn) => fn(value),
  getValue: () => value,
})
```

ここで `map` と `flatMap` のシグネチャはそっくりだけど、
引数部分が
`fn: (value: T) => U` と
`fn: (value: T) => Box<U>` という違いがあるね。

`flatMap` が受けとる関数は型としてBox型を返す必要があるという感じだね。
`map` が受け取る関数の型にはその制約がなくて、関数を呼んだあとにBoxに包まれる。


そして `flatMap`を使うと、ネストを防ぎながら箱を平坦化できる。

```typescript
const result = box(1)
  .flatMap(add(1))
  .flatMap(add(1))
  .getValue()

console.log(result) // 出力: 3
```

---

## 9. `flatMap` の力を使って流れを作る

`flatMap`を使って、さっきの処理をもっとシンプルに書いてみよう。

まず、ログを出力する`log`関数を作る。

```typescript
const log = <T>(label:string) => (x:T) => (console.log(`${label}: ${x}`), box(x))
```

次に、処理を組み立てる。

```typescript
box(1)
  .flatMap((x) => box(x + 1))
  .flatMap(log("after add 1"))
  .flatMap((x) => box(x * 2))
  .flatMap(log("after mul 2"))
  .flatMap((x) => box(x * 3))
  .flatMap(log("after mul 3"))
  .flatMap((x) => box(x * 4))
  .flatMap(log("after mul 4"))
  .flatMap((x) => box(`${x}!`))
  .flatMap(log("Final Result"))

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

**そう、`flatMap`はまるで「ロードローラーだッ!」**

ネストした箱をロードローラーのように平坦化しながら、次々と処理の道を突き進んでいく。

---

## 10. ロードローラー演算子

ここまでの話を聞いて、感のいい人は気づいたかもしれない。

`flatMap`というのは、`「箱の中の値に、箱を返す関数を適用する」` という `演算` っぽいよね。

`演算` には、`演算子（オペレーター）`が欲しくなるよね。

なので、演算子っぽく書けるようにしてみよう。

といっても、TypeScriptやJavaScriptにユーザーランドで演算子を作れる言語仕様はないので、Box型にそれっぽいプロパティを生やして対応してみるね。

```typescript
type Box<T> = {
  map: <U>(fn: (value: T) => U) => Box<U>
  flatMap: <U>(fn: (value: T) => Box<U>) => Box<U>
  ">>=": <U>(fn: (value: T) => Box<U>) => Box<U>
  getValue: () => T
}

const box = <T>(value: T): Box<T> => ({
  map: (fn) => box(fn(value)),
  flatMap: (fn) => fn(value),
  [">>="]: (fn) => fn(value),
  getValue: () => value,
})
```

flatMapのシグネチャと全く同じ、`>>=` という演算子を Box に追加したよ。

この演算子を便宜的に `ロードローラー演算子（便宜上）` とでも呼びますか。

JavaScriptやTypeScriptでは記号のようなプロパティは `[""]` をつかうことでアクセス可能となる。


### ロードローラー演算子（便宜上）を使って書くと

```typescript
box(1)
  [">>="]((x) => box(x + 1))
  [">>="](log("after add 1"))
  [">>="]((x) => box(x * 2))
  [">>="](log("after mul 2"))
  [">>="]((x) => box(x * 3))
  [">>="](log("after mul 3"))
  [">>="]((x) => box(x * 4))
  [">>="](log("after mul 4"))
  [">>="]((x) => box(`${x}!`))
  [">>="](log("Final Result"))

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

このような悪魔的記法で一連の流れを処理することが可能となる。

---

## 11. 関数を合成してもっとシンプルに

箱（Box）を返す関数の便利なところは、一つ一つの処理を関数として定義し、
それらの関数を合成して新しい関数が作成可能となることなんだ。


事前にBoxを返す関数を作成しておく。

ちなみに `add` や `mul` についてはカリー化（部分適用可能な形に）しておくと都合が良いね。

```typescript
const add = (y: number) => (x: number) => box(x + y)
const mul = (y: number) => (x: number) => box(x * y)
const excl = (x: number) => box(`${x}!`)
```

### `combinedMul` を定義する

まずはシンプルに、`>>=` で連結して `combinedMul` を作る。

```typescript
const combinedMul = (x: number) =>
  box(x)
    [">>="](mul(2))
    [">>="](log("after mul 2"))
    [">>="](mul(3))
    [">>="](log("after mul 3"))
    [">>="](mul(4))
    [">>="](log("after mul 4"))
```

### 全体の処理を組み立てる

```typescript
box(1)
  [">>="](add(1))
  [">>="](log("after add 1"))
  [">>="](combinedMul)
  [">>="](excl)
  [">>="](log("Final Result"))

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

関数を合成することで、処理の再利用性なんかもあがってくるよね。

---

## 12. `combinedMul` を一般化する

さっきの `combinedMul` だとまだまだ冗長で使い道よくわからないし、いちいち一個ずつ結合していてめんどくさいなと感じたよね。

なので、`combinedMul`を一般化して、任意の乗算を連結できるようにしよう。

```typescript
const combinedMul = (values: number[]) => (x: number) =>
  values.reduce(
    (acc, cur) => acc[">>="](mul(cur))[">>="](log(`after mul ${cur}`)),
    box(x)
  )
```

`combinedMul`の引数でかけ合わせたい数値の配列を受け取って、
その全ての数値をかけてはログ出力する関数を合成した関数を返す関数だね！


### 全体の処理を組み立てる（一般化バージョン）

```typescript
box(1)
  [">>="](add(1))
  [">>="](log("after add 1"))
  [">>="](combinedMul([2, 3, 4]))
  [">>="](excl)
  [">>="](log("Final Result"))

// 出力:
// after add 1: 2
// after mul 2: 4
// after mul 3: 12
// after mul 4: 48
// Final Result: 48!
```

これで好き放題掛け算しまくれるよね！

---

**まとめ**

`flatMap`を使うと、連続した処理を柔軟に書けるようになるかもしれないということがわかったね。
大事なのは、配列や Box が持つ「値に対して関数を適用する」という共通の性質を理解すること。

`flatMap`はまるでロードローラー。
あなたも `DIO` のようにロードローラーを使いこなしてください。


ちなみにあえて触れてはないですが、この考え方というのがいわゆるアレに通じます。
興味のある人はロードローラー演算子(便宜上)の `>>=` とかで調べたらいいかもしれません。
※ 知ってる人にとっては「ロードローラーってなんやねん」となりそうですが、そっとしといてください。。。