TypeScriptで理解するファンクター、アプリカティブファンクター、モナド
(evan-moon.github.io)本文
- ファンクターの
mapだけでは解決できない2つの問題(コンテナ内に関数が閉じ込められる問題、合成時に文脈が入れ子になる問題)から出発し、アプリカティブファンクターとモナドに至る過程をTypeScriptコードで説明 - 1988年にエウジェニオ・モッジ(Eugenio Moggi)がプログラムを
A → BではなくA → T(B)としてモデル化した背景から始める flatMap = map + joinという構造と、これを安全に使うための3つの法則(結合、左単位、右単位)を扱う- モナドがなぜ "自己関手圏のモノイド対象" なのかを、整数加算のモノイドと対比して説明
Promiseはモナディックに動作するが、厳密な数学的意味でのモナドではない理由にも触れる
ファンクターの限界: map ではできないこと
- カリー化された関数を
mapで適用すると、結果がMaybe<(b: number) => number>のように関数がコンテナ内に閉じ込められるmapはコンテナ外の関数しか受け取れないため、中に閉じ込められた関数を別の値に適用する方法がない
- ファンクターを返す2つの関数を合成すると、
Maybe<Maybe>のように文脈が入れ子になる- ステップが増えるほど
Maybe<Maybe<Maybe<...>>>と無限に入れ子になる
- ステップが増えるほど
アプリカティブファンクター: コンテナ内の関数を適用する
apply演算により、コンテナ内に閉じ込められた関数を別のコンテナの値に適用できるapply: T<(A → B)> → T<A> → T<B>
pure演算で純粋な値をコンテナに挿入する- 限界: どのコンテナを合成するかを事前に決めておく必要がある
- 前の計算結果を見て次の計算を決めるような、動的な逐次依存関係は表現できない
モナド: 入れ子を平坦化する演算の発明
join演算がT<T<A>> → T<A>として二重コンテナを単一に平坦化する- JavaScript の
Array.prototype.flatが同じ役割を果たす
- JavaScript の
- 実務では
map + joinを組み合わせたflatMapを使うflatMap: T<A> → (A → T<B>) → T<B>mapはA → Bを受け取るが、flatMapはA → T<B>を受け取ることで結果を1層に保つ
flatMap の3つの法則
- 結合法則: 三重入れ子
T(T(T(A)))を平坦化するとき、内側から平坦化しても
外側から平坦化しても結果は同じでなければならないm.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
- 左単位法則:
pureで入れてすぐにflatMapすると、関数を直接適用したのと同じpure(a).flatMap(f) === f(a)
- 右単位法則:
flatMapにpureを渡すと元のコンテナのままm.flatMap(pure) === m
"自己関手圏のモノイド対象" を分解する
- プログラミングにおけるファンクターは型の世界から型の世界へ向かうので、エンドファンクター(自己関手)である
- エンドファンクターそのものを対象とするエンドファンクター圏を構成できる
- モノイドの要件(二項演算 + 結合法則 + 単位元)に当てはめると:
- 二項演算 =
join - 単位元 =
pure - 整数加算のモノイドと構造が正確に対応する
- 二項演算 =
Promise はなぜモナドではないのか
thenが返り値に応じてmapとflatMapを混在させて処理するPromise<Promise>状態がランタイムで許容されず、即座に単一の
階層へ統合される- 実務上は便利だが、数学的なモナド則は満たさない
2件のコメント
Comonadについても扱ってください!
うあ…考えてみます(笑)