🎃

TSでApplicativeをなんとなく理解する

に公開

TL;DR

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

前回は TS で Monad を何となく学んでみました。
今までの学習から Functor や Monad は単なる Type Class であり、関数型プログラミングのデザインパターンの 1 つであることは理解できました。
今回は もう一つ代表的な例として挙げられる Applicative を学んでみます。

Applicative

アプリカティブはモナドに似ていますが少し違います。
モナドとの違いはモナドはモナディックな関数を直列に繋げるのに対して、アプリカティブでモナディックな関数を並列に組み合わせることが可能です。
例えば validation の必要がある場合、最初のエラーだけを残すのではなく、全てのエラーを結合するためにアプリカティブなアプローチを使うことになります。
一言で言えば、モナドは直列、アプリカティブは並列です。

モナドとアプリカティブの違いはよく validation の例で説明されます。

ここでは簡易的な Result 型を定義して、例を示してみます。

type Result<A> = { ok: true; value: A } | { ok: false; errors: string[] };

const Ok = <A>(a: A): Result<A> => ({ ok: true, value: a });
const Err = (msg: string): Result<never> => ({ ok: false, errors: [msg] });

const mapV = <A, B>(va: Result<A>, f: (a: A) => B): Result<B> =>
  va.ok ? Ok(f(va.value)) : va;

const nonEmpty = (field: string, s: string): Result<string> =>
  s.trim() ? Ok(s) : Err(`${field} は必須です`);

const positiveInt = (field: string, n: number): Result<number> =>
  Number.isInteger(n) && n > 0 ? Ok(n) : Err(`${field} は正の整数です`);

この validation をモナド(的)で実行しようとすると、次のようになります。

// 簡易的なモナド的直列合成
const chainV = <A, B>(va: Result<A>, f: (a: A) => Result<B>): Result<B> =>
  va.ok ? f(va.value) : va;

// 直列(name → age の順に評価)
function buildUserMonadic(
  name: string,
  age: number
): Result<{ name: string; age: number }> {
  return chainV(nonEmpty('name', name), (n) =>
    mapV(positiveInt('age', age), (a) => ({ name: n, age: a }))
  );
}

console.log(buildUserMonadic('', 0));
// { ok:false, errors: ["name は必須です"] }

buildUserMonadicには name が空文字で age が 0 のため、両方の validation が失敗することを期待します。
しかし、モナド的な直列合成を使うと、最初の検証が失敗した時点で処理が終了し、次の検証は行われません。
そのため、例えばフォームの validation の際に複数のエラーを取得することができず、ユーザーには最初のエラーしか表示されず、ユーザー体験が悪くなりがちです。

一方アプリカティブ(的)を使うと、次のように実装できます。

// 簡易的なアプリカティブ的並列合成
function combine<A, B>(va: Result<A>, vb: Result<B>): Result<[A, B]> {
  if (va.ok && vb.ok) return Ok([va.value, vb.value]);
  const ea = va.ok ? [] : va.errors;
  const eb = vb.ok ? [] : vb.errors;
  return { ok: false, errors: ea.concat(eb) };
}

// 2つの検証を“並べて”実行 → エラーを両方集める
function buildUserApplicative(
  name: string,
  age: number
): Result<{ name: string; age: number }> {
  const vName = nonEmpty('name', name);
  const vAge = positiveInt('age', age);

  const pair = combine(vName, vAge);
  return mapV(pair, ([n, a]) => ({ name: n, age: a }));
}

console.log(buildUserApplicative('', 0));
// { ok:false, errors: ["name は必須です", "age は正の整数です"] }

buildUserApplicativeでは、アプリカティブを使って独立した検証を同時に行い、両方のエラーを取得できます。
このように、アプリカティブは複数の独立した計算を並列に組み合わせることができ、全てのエラーを集めることができます。

参考

GitHubで編集を提案

Discussion