JavaScriptの配列型にはflatMap
というメソッドがある。flatMap
は同じく配列のメソッドmap
とflat
を連続して適用したのと同じ処理を一つのメソッドで行うものである。
[0, 10, 20].map(x => [x, x + 1]).flat(); // [0, 1, 10, 11, 20, 21]
[0, 10, 20].flatMap(x => [x, x + 1]); // [0, 1, 10, 11, 20, 21]
これflatMap
要るか?別に2回に分けて書けばよくないか?配列には他にもreverse
やfilter
やreduce
などたくさんのメソッドがあるのに、なぜflat
とmap
だけはそれらを組み合わせたflatMap
という専用メソッドがあるのか?
それには理由がある。なぜならflatMap
は関数型プログラミングにおいてとても重要で特別な関数だからである。
Monad
flatMap
のシグネチャーは以下のように表せる。ただし、実際のflapMap
ではthis
になる配列をここではinput
という引数で表している。
function flatMap(input: Array<T>, transform: T => Array<U>): Array<U>
この何の変哲もない関数シグネチャーは実は配列のflatMap以外のいろんな場所に現れる。
async 関数は flatMap です
例えばPromise。上のflatMap
のシグネチャーのArray
をPromise
に置き換えてみる。
function flatMap(input: Promise<T>, transform: T => Promise<U>): Promise<U>
このシグネチャーはasync関数に現れている。以下のコードのasync関数fn1
を考えてみる。
function fn2(): Promise<number>;
async fn1(): Promise<string> {
const n = await fn2();
if(n === 0) throw new Error();
return n.toString();
}
このfn1
のやっているasync
await
とは何か、それは以下のようなflatMap
なのであると言える。
flatMap(fn2(), n => {
if(n === 0) return Promise.reject(Error());
return Promise.resolve(n.toString());
});
関数シグネチャーを確認してみてほしい。ちゃんとflatMap
の形式になっていることがわかる。ここではflatMap
はPromise
をinput
として受け取り、それがresolveされた時にtransform
を実行してその返り値を返すような関数となる(例外が投げられた場合は省いた説明)。
function flatMap(input: Promise<T>, transform: T => Promise<U>): Promise<U> {
return new Promise((resolve, reject) => {
input.then(x => {
const next = transform(x);
next.then(y => resolve(y));
next.catch(e => reject(e));
});
input.catch(e => reject(e));
});
}
オプショナルチェーン(?.
演算子)は flatMap です
他にもオプショナルチェーン。今度は以下のようなシグネチャーを考える。これもこれまでのflatMap
と同じ形式をしている。
type Optional<T> = T | undefined;
function flatMap(input: Optional<T>, transform: T => Optional<U>): Optional<U>
以下のコードを考える。
function fn3(): Map<string, number> | undefined;
const n: number | undefined = fn3()?.get('key');
このオプショナルチェーン演算子(?.
)がやっていることは何か、これも以下のようなflatMap
なのであると言える。
const n = flatMap(fn3(), m => m.get('key'));
関数シグネチャーがflatMap
の形式になっていることを確認してほしい。ここではflatpMap
はinput
がundefined
ならそのままundefined
を返し、undefined
以外ならtransform
を実行してその返り値を返す関数となる。オプショナルチェーンがやっているのはこういうことだったであろう。
function flatMap(input: Optional<T>, transform: T => Optional<U>): Optional<U> {
if(input === undefined) return undefined;
else return transform(input);
}
関数型言語では
このようにflatMap
のパターンは配列だけでなくいろいろな場所に現れるという事がわかる。このパターンは非常に便利で汎用的に使えるのでMonadという名前までついている。
JavaScriptではこれら様々な場所に現れるflatMapを対象の型ごとに別の構文で記述していた。つまり、配列であればflatMap
メソッド、Promise
であればasync
await
、オプショナル型であれば?.
という様に。しかしこれらが同じ形式の操作を行っているという事なら同じ構文で記述できるのでは?それは実際その通りで、関数型言語ではこれらを全く同じ構文で記述できたりする。例えばScalaではこれらは以下のように全て同じ形式で記述できる。
// Array flatMap
for {
x <- Vector(0, 10, 20)
y <- Vector(x, x + 1)
} yield y
// Async await
for {
n <- fn21()
_ = if (n == 0) throw new Exception() else ()
} yield n.toString
// Optional chain
for {
x <- fn3()
y <- x.get("key")
} yield y
上のfor
を使った構文はfor内包表記と呼ばれている。for内包表記は抽象操作flatMapを行うための専用構文である。つまりScalaは言語機能として組み込むほどflatMapを重要視している。
まとめ
ということで配列のflatMap
メソッドは単なるflat
とmap
の組み合わせ以上の意味を持っていることが分かった。flatMap
の操作はMonadという概念で抽象化することができる。この抽象化されたflatMapはプログラム上の様々な場所で現れ、コードをシンプルに記述するのに便利なパターンであることが分かった。それだけ重要な関数なのでflat
もmap
もあるのにあえてflatMap
を標準に入れたんじゃないかと思っている(これはまあ想像だが)。
参考資料
- なっとく!関数型プログラミング
-
https://youtu.be/C2w45qRc3aU?si=G4dUxB9AMamGEKa2
- 本稿では単に同じパターンがありますねという説明しかしてないが、Monadの意味論的な説明もしているので有益です