コンピュテーション式の導入
LT;DR
これにより、bind や map 、深くネストした Result 生成関数を扱う必要がなくなる ちょっと解像度が低い ?? radish-miyazaki.icon Result を無視した実装からの移行コストも軽減できる
ただし、Result のコンピュテーション式は公式で提供されていないので、独自実装が必要
リストを検証する際には、Result<..., ...> list を Result<... list, ...> を変換する必要が出てくる
この場合、以下のような prepend 関数を用いた sequence で実現する
prepend: 1 つの Result と Result のリストを受け取り、 リストの先頭要素に追加(cons)する関数 code:fsharp
let prepend firstR restR =
match firstR, restR with
| Ok first, Ok rest -> Ok (first::rest)
| Error err1, Ok _ -> Error err1
| Ok _, Error err2 -> Error err2
| Error err1, Error err2 -> Error err1
sequence: prepend をリストの各要素に適用する
code:fsharp
let sequence aListOfResults =
let initialiValue = Ok []
List.foldBack prepend aListofResults initialValue
上の実装では単一のエラーしか保存できないが、アプリカティブ を用いることで複数エラーに対応できる 一般的に、prepend と sequence は Result モジュールで定義する
code:fsharp
module Result =
let prepend firstR restR = ...
let sequence aListOfResults = ...
hr.icon
なぜコンピュテーション式が必要か
e.g. Result.bind や map を何回も呼び出す、深くネストした Result 生成関数を扱う...
しかし、Result の コンピュテーション式は公式で提供されていない
そのため、独自実装が必要
今回は モナド として扱うため、Bind と Return のみを実装する code:fsharp
type ResultBuilder() =
member this.Return(x) = Ok x
member this.Bind(x, f) = Result.bind f x
let result = ResultBuilder()
どう変わるか?
導入前
code:fsharp
let placeOrder unvalidatedOrder =
unvalidatedOrder
|> Result.bind priceOrderAdapted
|> Result.map acknowledgeOrder
|> Result.map createEvents
Result を返す validateOrder の出力と priceOrder の入力を接続している
導入後
code:fsharp
let placeOrder unvalidatedOrder =
result {
let! validatedOrder =
validateOrder unvalidatedOrder
|> Result.mapError PlaceOrderError.Validation
let! pricedOrder =
priceOrder validatedOrder
|> Result.mapError PlaceOrderError.Pricing
let acknowledgeOrder =
acknowledgeOrder pricedOrder
let events =
createEvents pricedOrder acknowledgementOption
return events
}
全体を result コンピュテーション式でラップし、bind を let! に置き換える
それ以外は通常の構文を利用する(Result.map は不要)
これにより、validateOrder や priceOrderの出力を、Result を意識することなく直接扱えるように
構文の詳細
let!: 結果を アンラップ して内部値を取得する たとえば validatedOrder は pricedOrder に直接渡せる通常の値
エラー型はブロック全体で統一されている必要があるため、Result.mapError で 型を持ち上げる return: 値を ラップ して呼び出し元に返す コンピュテーション式の合成
e.g.
以下のような 2 つのコンピュテーション式があったとする
code:fsharp
let validatedOrder input = result {
let! validatedOrder = ...
return validatedOrder
}
let priceOrder input = result {
let! pricedOrder = ...
return pricedOrder
}
これらは、より大きな result 式のなかで利用できる
code:fsharp
let placeOrder unvalidatedOrder = result {
let! validatedOrder = validatedOrder unvalidatedOrder
let! pricedOrder = priceOrder validatedOrder
...
}
既存の関数を置き換える
Result を無視した「検証のステップ」の実装に Result を適用する
すべてのヘルパ関数が Result を返すようになると、このコードは機能しなくなる
e.g. OrderId.create が Result<OrderId, string> を返す
しかし、result コンピュテーション式を用いれば、実装の構造をほとんど変えずに適用できる
code:fsharp
let validateOrder: ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
result {
let! orderId =
unvalidatedOrder.OrderId
|> OrderId.create
|> Result.mapError ValidationError
let! customerInfo =
unvalidatedOrder.CustomerInfo
|> toCustomerInfo
let! shippingAddress =
unvalidatedOrder.ShippingAddress
|> toAddress checkAddressExists
let! billingAddress = ...
let! lines = ...
// すべてのフィールドの準備ができたら、それを元にレコード型を作成して返す
let validatedOrder: ValidatedOrder = {
OrderId = orderId
CustomerInfo = customerInfo
ShippingAddress = shippingAddress
BillingAddress = billingAddress
OrderLines = lines
}
return validatedOrder
}
変更箇所
コード全体を result コンピュテーションでラップ
let を let! に
ただし、toCustomerInfo と toAddress のエラー型は ValidationError であると仮定して、mapError を呼び出していない
Result のリストを扱う
Result を無視して明細行をチェックする場合、List.map を呼び出すだけで良かった
toValidatedOrderLine ヘルパ関数が Result を返すようになると、このコードは機能しなくなる
リストの Result(Result<ValidatedOrderLine list, ValidationError> )が欲しいが、得られるのは Result のリスト( Result<ValidatedOrder, ValidationError> list)
code:fsharp
let validateOrder unvalidatedOrder =
...
let lines = // Result のリスト
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkAddressExists)
let validatedOrder = {
...
Lines = lines // 欲しいのはリストの Result なのでコンパイルエラー
}
解決するには、Result のリスト を リストの Result に変換する関数が必要
実装方針
Result のリストをループし、1 つでも失敗があれば全体の結果はエラーとなる
すべて成功すれば、全体の結果は成功値のリストとなる
どう実装する?
1 つの Result と Result のリストを受け取り、 リストの先頭要素に追加(cons)する関数を実装する 両方 が Ok であれば、中身を cons し、できたリストを再び Result でラップする
それ以外の場合は、エラーを返す
code:fsharp
// val prepend:
// firstR: Result<'a,'b> -> restR: Result<'a list,'b> -> Result<'a list,'b>
let prepend firstR restR =
match firstR, restR with
| Ok first, Ok rest -> Ok (first::rest)
| Error err1, Ok _ -> Error err1
| Ok _, Error err2 -> Error err2
| Error err1, Error err2 -> Error err1
prepend を使うと、Result のリスト を リストの Result に変換する関数 を実装できる
code:fsharp
module Result =
// val sequence: aListOfResults: Result<'a,'b> list -> Result<'a list,'b>
let sequence aListOfResults =
let initialValue = Ok []
List.foldBack prepend aListOfResults initialValue
List.foldBack で最後から順に反復処理し、リストの Result に cons していく
bind や map 、 mapError などと同様に Result モジュールに実装する
呼び出し方
成功時
code:fsharp
let IntOrError = Result<int, string>
let successResult =
Result.sequence listOfSuccesses // Ok 1; 2 失敗時
code:fsharp
let errorResult =
Result.sequence listOfSuccesses // Error "bad"
すべてのエラーを保存したい場合は アプリカティブ を用いれば実現できる code:fsharp
// val apply: fResult: Result<('a -> 'b)> -> xResult: Result<'a> -> Result<'b>
let apply fResult xResult =
match fResult,xResult with
| Success f, Success x ->
Success (f x)
| Failure errs, Success x ->
Failure errs
| Success f, Failure errs ->
Failure errs
| Failure errs1, Failure errs2 ->
validateOrder に適用する
code:fsharp
let validateOrder: ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
result {
let! orderId = ...
let! customerInfo = ...
let! pricedOrder = ...
let acknowledgeOrder = ...
let! lines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkAddressExists)
|> Result.sequence
let validatedOrder: ValidatedOrder = {
...
Lines = lines
}
return validatedOrder
}
パフォーマンスが気になる場合、List.map と Result.sequence をまとめれば良い
Haskell でも Traversable(データ構造を走査する) のメソッドとして提供している radish-miyazaki.icon code:haskell
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
以下のような型シグネチャの関数を作成するイメージ
code:fsharp
(UnvalidatedOrderLine -> Result<ValidatedOrderLine, ValidationError>)
-> UnvalidatedOrderLine list
-> Result<ValidatedOrderLine list, ...>