7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】flat と map があるのに flatMap は必要なのか?【Monad】

Posted at

JavaScriptの配列型にはflatMapというメソッドがある。flatMapは同じく配列のメソッドmapflatを連続して適用したのと同じ処理を一つのメソッドで行うものである。

[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回に分けて書けばよくないか?配列には他にもreversefilterreduceなどたくさんのメソッドがあるのに、なぜflatmapだけはそれらを組み合わせたflatMapという専用メソッドがあるのか?

それには理由がある。なぜならflatMap関数型プログラミングにおいてとても重要で特別な関数だからである。

Monad

flatMapのシグネチャーは以下のように表せる。ただし、実際のflapMapではthisになる配列をここではinputという引数で表している。

function flatMap(input: Array<T>, transform: T => Array<U>): Array<U>

この何の変哲もない関数シグネチャーは実は配列のflatMap以外のいろんな場所に現れる。

async 関数は flatMap です

例えばPromise。上のflatMapのシグネチャーのArrayPromiseに置き換えてみる。

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の形式になっていることがわかる。ここではflatMapPromiseinputとして受け取り、それが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の形式になっていることを確認してほしい。ここではflatpMapinputundefinedならそのまま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メソッドは単なるflatmapの組み合わせ以上の意味を持っていることが分かった。flatMapの操作はMonadという概念で抽象化することができる。この抽象化されたflatMapはプログラム上の様々な場所で現れ、コードをシンプルに記述するのに便利なパターンであることが分かった。それだけ重要な関数なのでflatmapもあるのにあえてflatMapを標準に入れたんじゃないかと思っている(これはまあ想像だが)。

参考資料

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?