😸

TSでMondadを理解する

に公開

TL;DR

  • fp-ts の Introduction series to FP-TS を読んで、関数型を学習してみるシリーズ。
  • Monad を TS で学んでみる。

Monad

モナドとは、プログラミングパターンの 1 つでモナディックな関数を直列に繋げることができるものを指します。
モナディックとは通常の値を受け取り、ある種の拡張された値を返す関数のことです。

“モナディックな関数”とは

まず以下のような関数を考えてみます。

const toUpper = (s: string): string => s.toUpperCase();

この関数は入力として string(通常の値)を受け取り、出力として string(通常の値)を返す関数です。

一方 モナディックな関数 は “箱” 付きの値 を返します。
その箱こそが モナド で、M<T> の形をしています。

type Option<A> = { _tag: 'Some'; value: A } | { _tag: 'None' };

const parseIntOpt = (s: string): Option<number> =>
  isNaN(Number(s)) ? { _tag: 'None' } : { _tag: 'Some', value: Number(s) };

この関数は入力として string(通常の値)を受け取り、出力として Option<number>という「値があるかもしれない」という 拡張された文脈 付きの値を返す関数です。

一方 モナディックな関数 は “箱” 付きの値 を返します。

この説明からすると、ここでいう箱はOptionということになりますが、箱と言われてもあまりイメージできなません。
そこで「値と文脈を 1 つに包んだラッパー」と言い換えてみます。
どういうことかと言うと、値は実際に欲しいデータで、文脈はその値にまつわる追加の意味と言えます。
先ほどのparseIntOpt関数で言うと、値はnumberで、文脈はOptionという「この値はないかもしれない」という意味を持っています。
(Option 以外にも Result や Either などもある。)

このようにモナディックな関数は、値とその値にまつわる文脈を 1 つに包んだラッパーを返す関数ということは理解できました。
ではそのモナディックな関数を直列に繋げるとはどういうことでしょうか?

モナドの条件

モナドとして成立させるには次の 3 つの要素を満たす必要があります。

  • データ構造
  • これらの関数: of (return, pure) / flatMap (bind, chain)
  • 関数がどのように動作しなければならないかについてのルール (モナド則)

1 つ目のデータ構造に関していは先ほどから出てきたOption等がそれにあたります。
2 つ目の関数らは、次のような関数です。

interface Monad<M<_>> {
  of:      <A>(a: A) => M<A>
  flatMap: <A, B>(ma: M<A>, f: (a: A) => M<B>) => M<B> // 直列に繋げると部分で重要
}

of は普通の値を文脈付きの値に変換する関数で、flatMap は文脈付きの値を受け取り、別の文脈付きの値を返す関数です。
このようにモナドは offlatMap の 2 つの関数を持つ、前回学んだ Type Class として定義できます。

ではこの Monad Type Class を用いて、使用例を見ていきます。
文字列 10 を数値に変換し、2 で割る処理を考えます。
parseIntOptsafeDivideもモナディックな関数です。

const parseIntOpt = (s: string): Option<number> =>
  isNaN(Number(s)) ? none : some(Number(s));

const safeDivide =
  (den: number) =>
  (num: number): Option<number> =>
    den === 0 ? none : some(num / den);

これを Monad を使用せず、計算すると次のようになります。

const result: Option<number> = (() => {
  const nOpt = parseIntSafe('10');
  if (isNone(nOpt)) return none; // 失敗したら即終了

  const dOpt = parseIntSafe('2');
  if (isNone(dOpt)) return none; // 失敗したら即終了

  return divideSafe(dOpt.value)(nOpt.value);
})();

一回一回の関数の計算結果ごとに null チェックを行う必要があり、コードが冗長になってしまいます。
しかし OptionMonad を使うことで、より簡潔に書くことができます。
(pipe を使えばより見通しがいいですが、ここでは省略します。)

type Option<A> = { _tag: 'Some'; value: A } | { _tag: 'None' };

const OptionMonad = {
  of: <A>(value: A): Option<A> => ({ _tag: 'Some', value }),
  flatMap: <A, B>(ma: Option<A>, f: (a: A) => Option<B>): Option<B> =>
    ma._tag === 'Some' ? f(ma.value) : { _tag: 'None' },
} as const;

// 直列につなぐ
const result = OptionMonad.flatMap(parseIntOpt('10'), (n) =>
  OptionMonad.flatMap(parseIntOpt('2'), (d) => safeDivide(d)(n))
);

console.log(result); // => { _tag: 'Some', value: 5 }

前の関数の結果を受け取り、それを使って次の関数を実行するように処理をワンラインに並べること(直列に繋ぐ)で、都度の null チェックを省略できます。
(3 つ目のモナド則は今回省略します。)

まとめ

このようにモナドは、値とその値にまつわる文脈を 1 つに包んだラッパーを返す関数であり、offlatMap の 2 つの関数を持つ Type Class として定義できます。
モナドを使うことで、値の文脈を意識しながら関数を直列に繋げることができ、コードの冗長性を減らすことができます。

GitHubで編集を提案

Discussion