fp-tsでのパイプライン合成

TypeScriptのライブラリ fp-tsは関数型プログラミングにおける典型的なデータ型などの抽象化を提供しています。各種のデータ型はpipeとchainによって合成可能です。
2020.04.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

最近TypeScriptを書く機会があってfp-tsというライブラリを触ってみたので少し紹介したいと思います、

fp-tsは関数型プログラミングでおなじみの抽象化を提供するTypeScriptのライブラリです。

具体的には以下のものが提供されています。

  • 関数(Identity, FunctionN, Lazy, ...etc)
  • データ型(Option, Either, IO, ...etc)
  • 型クラス(Functor, Applicative, Monad, ... etc)
  • 高カインド型 (TypeScriptでは高カインド型をサポートしていないのでLightweight higer-kind polymorphism という手法で実現している)

よく使いそうなデータ型

よく使いそうなデータ型は以下かと思います。

  • IO 同期実行される失敗しない計算
  • Task 非同期実行される失敗しない計算
  • TaskEither 非同期実行される失敗する可能性のある計算(中身はTask)
  • ReaderT, OptionTなどのモナドトランスフォーマー

chainによるモナドの合成

でもTypeScriptにはdo構文がないけどどうしたらいいの・・・bindのネストはつらいのよと思うことでしょう。fp-tsではこのような場合pipe関数とChainを組み合わせることでパイプラインを構築します。

pipe

pipeにはいくつかの定義がありますが、以下に一部を抜粋して紹介します。

export function pipe<A>(a: A): A
export function pipe<A, B>(a: A, ab: (a: A) => B): B
export function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
export function pipe<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D

Chain

fp-tsにはChainという型クラスがありMonadはこれを継承しています。Chainの定義は以下の通りです。

ここでHKT<F, A>は高カインド型の値、Fは型コンストラクタ、AはFの型パラメータです。

export interface Chain<F> extends Apply<F> {
  readonly chain: <A, B>(fa: HKT<F, A>, f: (a: A) => HKT<F, B>) => HKT<F, B>
}

実際には各データ型に定義されたカリー化された以下のような関数を使います。

const chain<A, B, F> = (f: (a: A) => HKT<F, B>) => (fa: HKT<F, A>) => HKT<F, B>

これをpipeと組み合わせて以下のようにパイプラインを構築できます。

パイプラインの例

この例ではpipeの2つ目のパラメータはchainの戻り値 Option<number> => Option<string> となりpipeの1つめのパラメータを受け取って新たなOptionを返す関数となっています。

import * as O from 'fp-ts/lib/Option'
import { identity } from 'fp-ts/lib/function'
import { pipeable } from 'fp-ts'

pipeable.pipe(
  O.some(42),
  O.chain(n => n === 42 ? O.some('Answer to Everything') : O.none),
  O.fold(() => 'No Answer', identity)
) // Answer to Everything

パイプライン途中の計算結果の束縛

pipeでは合成する各ステップの結果を変数に束縛することができないので直前のステップ以外の結果を参照する場合には次のような工夫が必要です。

  • A: 後続のステップで必要な値をタプルで引回す
  • B: 必要な値を参照する処理を1つのchainにまとめる

Bは具体的には以下のようになります。

import * as O from 'fp-ts/lib/Option'
import { pipeable } from 'fp-ts'

//ドラえもんのBMI
const getWeight = () => O.some(129.3)
const getHeight = () => O.some(129.3)
pipeable.pipe(
  getWeight(),
  O.chain(w =>
    pipeable.pipe(getHeight(),
      O.chain(h => O.some(w / Math.pow(h, 2))
    )
  )
) // 77.34!

まとめ

fp-tsでは関数型プログラミングにおける典型的なデータ型などの抽象化を提供しています。各種のデータ型はpipeとchainによって合成可能です。