[英]"Merging" Discriminated Unions in F#?
繼此問題之后,我遇到了將不同類型的Result
類型組合在一起的問題。
(以下是人為的例子,不是真正的代碼)
假設我有一個讀取文件的函數:
type ReadFileError =
| FileNotFound of string
let readFile (path : string) : Result<string, ReadFileError> =
// --- 8< ---
還有一個以某種方式解析它的函數:
type JsonParseError =
| InvalidStructure of string
let parseJson (content : string) : Result<Json, JsonParseError> =
// --- 8< ---
現在我可以組合這些來創建一個讀取和解析文件的函數:
type ReadJsonError =
| ReadFileError of ReadFileError
| JsonParseError of JsonParseError
let readJson (path : string) : Result<Json, ReadJsonError> =
match path |> readFile with
| Ok content ->
match content |> parseJson with
| Ok json -> Ok json
| Error e -> Error (ReadJsonError.JsonParseError e)
| Error e -> Error (ReadJsonError.ReadFileError e)
如您所見,統一錯誤類型相當笨拙。 我需要定義一個新的聯合類型並正確包裝Error
端。 對於基於異常的方法,這不是您必須擔心的事情,因為 throw 在類型方面是開放式的。
組合不同類型的錯誤時是否可以使Result
樣式方便?
首先簡短的回答。 稍后我會回來提供更長的答案。
如果您正在構建一個單體應用程序,建議為整個應用程序創建一個錯誤類型:
type AllErrors =
| FileNotFound of string
| InvalidJsonStructure of string
| OtherErrors ...
這將為您提供一個定義所有錯誤的好地方,您可以創建統一的printError
和其他錯誤處理函數。
有時這是不可能的,例如,如果您的代碼是模塊化的,並且每個模塊都有自己的 ErrorType,那么您有兩個選擇,仍然創建一個唯一的類型並映射到它,或者像您一樣創建一個嵌套的組合類型。 這是你的決定。 在這兩種情況下,您都使用Result.mapError
從語法上講,有很多方法可以做到這一點。 為了避免嵌套match
你使用Result.bind
和Result.mapError
let readJson (path : string) : Result<Json, ReadJsonError> =
readFile path
|> Result.mapError ReadFileError
|> Result.bind (fun content ->
parseJson content
|> Result.mapError JsonParseError
)
如果你有一個result
計算表達式:
type Builder() =
member inline this.Return x = Ok x
member inline this.ReturnFrom x = (x:Result<_,_>)
member this.Bind (w , r ) = Result.bind r w
member inline this.Zero () = Ok ()
let result = Builder()
那么它看起來像這樣:
let readJson (path : string) : Result<Json, ReadJsonError> = result {
let! content = readFile path |> Result.mapError ReadFileError
return! parseJson content |> Result.mapError JsonParseError
}
與運營商:
let (>>= ) vr f = Result.bind f vr
let (|>>.) vr f = Result.mapError f vr
可能是這樣的:
let readJson (path : string) : Result<Json, ReadJsonError> =
readFile path |>>. ReadFileError
>>= fun content ->
parseJson content |>>. JsonParseError
或這個:
let readJson (path : string) : Result<Json, ReadJsonError> =
path
|> readFile
|>>. ReadFileError
>>= fun content ->
content
|> parseJson
|>>. JsonParseError
甚至這個:
let readJson (path : string) : Result<Json, ReadJsonError> =
path |>
readFile |>>.
ReadFileError >>=
fun content ->
content |>
parseJson |>>.
JsonParseError
好吧,最后一個只是為了好玩。 我不提倡你這樣編碼。
您也可以簡單地創建功能的統一版本:
let readFileU = readFile >> Result.mapError ReadFileError
let readJsonU = parseJson >> Result.mapError JsonParseError
並將它們與 Kleisli 運算符綁定:
let (>=>) f g p = f p |> Result.bind g
let readJson = readFileU >=> readJsonU
結合錯誤類型是Result
一個問題,我只有在嘗試時才意識到。
除了例外,這是通過讓所有例外繼承基類來“解決”的。 所以一種類似的方法可能是type R<'T> = Result<'T, exn>
但是,我發現這並不吸引人,並且通常會陷入我定義自己的 Result 類型的模式,該類型允許同類類型的聚合失敗。
有點像這樣
type BadResult = Message of string | Exception of exn
type BadTree = Leaf of BadResult | Fork of BadTree*BadTree
type R<'T> = Good of 'T | Bad of BadTree
另一種方法是使用Choice
組合Result
失敗。 不確定一個人最終會不會因為這個而進入一個特別有吸引力的地方。
let bind (t : Result<'T, 'TE>) (uf 'T -> Result<'U, 'UE>) : Result<'U, Choice<'TE, 'TU>> = ...
這可能根本沒有幫助你,但也許它會產生一些關於如何進行的想法?
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.