TL;DR

  • TypeScript では純粋関数と副作用を型で区別できないので、Functional Core, Imperative Shell の考え方で両者を分ける。
  • Result でエラーを値として扱って純粋関数を増やし、副作用は処理の端に置く。分離はディレクトリ構造と dependency-cruiser で守る。

内容

副作用との付き合い方

副作用をゼロにすることは不可能なので、純粋関数を増やすことと、副作用を分離することの 2 つに取り組む。

TypeScript の型の限界

TypeScript の型は表現力が高いが、限界もある。純粋関数と副作用を持つ関数を、型のレベルで区別できない(Int -> IO Int のような表現ができない)。Java の package のように、モジュールの境界を言語レベルで強制する仕組みもない。

Functional Core, Imperative Shell

そこで、言語の思想には逆らわず、副作用と純粋関数を分離する。 そのための考え方が Functional Core, Imperative Shell(FCIS)である。純粋関数(Functional Core)を中心に置き、副作用(Imperative Shell)を外側に置く。永続化を端に追いやるのと同じ考え方である。

実例

1つ目は Result でエラーを値として扱い、純粋関数を増やすことである。

type Result<T, E> = { isOK: true; value: T } | { isOK: false; value: E }

これにより、バリデーションのロジックが純粋関数になり、エラーが型に現れる。

2つ目は副作用を処理の端(最初か最後)に置くことである。たとえば「入力 → バリデーション → エラーの集約 → ディスパッチ → 出力」という流れでは、副作用であるディスパッチを後ろに置く。

その結果、テストしやすくなり、副作用の処理が一か所に集まり、バリデーションを足しやすくなった。

ディレクトリ構造で FCIS を守る

TypeScript には IO 型も package の機能もないので、ディレクトリ構造で FCIS を実現する。ディレクトリの配置を決め、依存関係は dependency-cruiser で機械的に制限する。そのチェックは Lefthook で強制する。

キーワード

関連