簡體   English   中英

在 F# 中“合並”受歧視的聯合?

[英]"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.bindResult.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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM