TL;DR
- ライブラリや外部サービスが投げる例外をドメインエラーとして扱いたいときは、例外を
Resultに変換するアダプタブロックを作り、2 トラックモデルに組み込む。 - 何も返さない関数は、入力をそのまま返す関数に変換してから
Result.mapを適用すれば、2 トラックモデルに組み込める。
内容
Domain Modeling Made Functional 第10章 Result を生成する関数の連鎖(bind と errorMap、map)では 1 トラック関数とスイッチ関数だけを扱ったが、ほかの「形」の関数もある。ここでは、例外を投げる関数と、何も返さない関数の 2 種類を扱う。
例外を投げる関数
自分たちのコードで例外を投げないようにしても、ライブラリのように自分たちが管理していないコードは例外を投げる。自分たちで投げた例外(パニック)はドメインの一部ではないので、最上位でだけ捕捉する(Domain Modeling Made Functional 第10章 ドメインエラーを扱う)。一方、ライブラリなどの例外はドメインエラーとして扱いたい場合がある。そのときは、例外を投げる関数を Result を返す関数に変換するアダプタブロックを作る。
Domain Modeling Made Functional 第10章 実際のパイプラインでの bind と map の利用を拡張し、外部サービスのタイムアウトを捕捉して RemoteServiceError に変換する例を示す。
type ServiceInfo = {
Name: string
Endpoint: Uri
}
type RemoteServiceError = {
Service: ServiceInfo
Exception: System.Exception
}
let serviceExceptionAdapter serviceInfo serviceFn x =
try
Ok (serviceFn x) // サービスを呼び出して成功を返す
with
| :? TimeoutException as ex ->
Error { Service = serviceInfo; Exception = ex }
| :? AuthorizationException as ex ->
Error { Service = serviceInfo; Exception = ex }注目すべきは、すべての例外ではなく、ドメインに関係するものだけを捕捉している点である。サービスが 2 つのパラメータを取る場合は、serviceFn x y を呼ぶ別の定義が必要になる。ここまでのアダプタブロックは汎用的だが、特定のサービスに絞ったものを用意したほうがよい場合もある。たとえばデータベースの例外を、「レコードが見つからない」「キーが重複している」といったドメインに合ったケースを持つ選択型に変換する。
呼び出し側は次のようになる。
let serviceInfo = {
Name = "AddressCheckingService"
Endpoint = ...
}
// 例外を発生させるサービス
let checkAddressExists = ...
// val checkAddressExists: UnvalidatedAddress -> Result<CheckedAddress, RemoteServiceError>
let checkAddressExists address =
let adaptedService =
serviceExceptionAdapter serviceInfo checkAddressExists
adaptedService addressこれを 2 トラックモデルに組み込むには、まず PlaceOrderError に RemoteServiceError のケースを足す。
type PlaceOrderError =
| Validation of ValidationError
| Pricing of PricingError
| RemoteService of RemoteServiceErrorそして、mapError で共通のエラー型に変換する。
let checkAddressExists address =
let adaptedService =
serviceExceptionAdapter serviceInfo checkAddressExists
address
|> adaptedService
|> Result.mapError RemoteService何も返さない関数
次のように I/O に書き込むだけの関数は、何も返さない。
// val logError: msg: string -> unit
let logError msg = printfn "ERROR %s" msgこの関数を 2 トラックモデルに組み込むには、まず、受け取った入力で何も返さない関数を呼び出し、元の入力をそのまま返すパススルー関数を作る。ここでは tee と呼ぶ(Domain Modeling Made Functional 第9章 最初のステップの実装の関数アダプタと同じ考え方である)。
// val tee: f: ('a -> unit) -> x: 'a -> 'a
let tee f x =
f x
xtee を適用した関数に Result.map を適用すれば、2 トラック関数に変換できる。
// val adaptDeadEnd: f: ('a -> unit) -> (Result<'a,'error> -> Result<'a,'error>)
let adaptDeadEnd f = Result.map (tee f)