UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【DMMF】Railway Oriented Programmingのエラーハンドリングの話

Sansanさんの記事でTypeScript開発にRailway Oriented Programmingを持ち込んでエラーハンドリングする話を読んだ

buildersbox.corp-sansan.com

DMMF本の内容のおさらいができて良い記事だった

話の内容は、よくある手続的な実装処理の中で、

  • 精査した結果を単純なbooleanで返却して分岐中で例外をスローしていたり
  • DBへの更新時に例外スローされたり
  • 非同期処理の結果をResult型でsuccessかどうかで分岐して例外をスローしていたり

と、多様な形で表現されたエラーが混在すると、以下の課題がある

  • 『関数のパイプライン』のイメージが崩れてしまう
  • エラー表現が多様なので読みにくい
  • try-catchのcatchでエラーを集約させる書き方だとエラーの型情報が失われてしまう(catch節の中で網羅的に考慮できているかどうかは不明)

そこでRailway Oriented Programmingの考え方を取り入れたという話

Railway Oriented Programming

1つのインプットで2つのアウトプットをするような関数(スイッチ関数)を以下の図のようにくっ付けて2レーンにする手法

手順

  1. 関数の出力をResult型で表現する
  2. 関数にResult型を入力できるようにする
  3. 関数を連結する
  4. エラーハンドリングする

まず、こういうResult型を用意する

type Failure = { errorCode: number };
type Result<Ok, Ng extends Failure> = {
    success: true;
    data: Ok;
} | {
    success: false;
    error: Ng;
}

そして、Result型をインプットにして、またResult型を返す関数を用意する

この時「Result型の成功データをインプットにして、またResult型を返す関数」をインジェクションできるようにしておく

function bypass<
  PreviousOk,
  PreviousNg extends Failure,
  NextOk,
  NextNg extends Failure
>(
  func: (i: PreviousOk) => Result<NextOk, NextNg>,
): (input: Result<PreviousOk, PreviousNg>) => Result<NextOk, PreviousNg | NextNg> {
  return (input) => input.success ? func(input.data) : input;
}

それをpipeで繋いで、最後はResult型のデータをts-patternのmatch関数でハンドリングする

  ...
  (result) => match(output)
      .with({ error: { errorCode: 400 } }, () => { ... })
      .with({ error: { errorCode: 500 } }, () => { ... })
      .with({ success: true }, () => {})
      .exhaustive()

これで確かに網羅的にエラーがハンドリングできて良さそう