在F#中如何“合并”带有判别联合标签的数据类型?

3

继续上一个问题(这个问题)的讨论,我在将不同类型的 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 样式更加方便?

2个回答

3

首先,简短明了的回答:如果您正在构建单体应用程序,则建议为整个应用程序创建仅一个错误类型。

稍后会有更为详细的回答。

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

3

将错误类型结合在一起是使用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>> = ...

这可能对你毫无帮助,但也许可以启发一些想法来继续进行?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接