F# - 嵌套类型

F# - Nested types

我试图理解使用 F# 的函数式编程,为此我开始了一个小项目,但我 运行 在以下问题中似乎找不到任何优雅的解决方案。 我创建了 Validation<'a>,它是非常专业的 F# 结果:Result<'a, Error list>,它帮助我处理验证结果。

我有两个函数都使用签名执行一些验证:

'a -> Validation<'b>

还有第三个函数使用经过验证的带有签名的参数:

'a -> 'b -> Validation<'c>

我想实现的是:

  1. 验证参数'a
  2. 如果参数 'a 的验证通过,则验证参数 'b
  3. 如果参数 'b 的验证通过,则将参数 'a 和 'b 提供给最终函数

到目前为止,我使用 apply 函数来实现这种行为,但是当我尝试在这种情况下使用它时,结果类型是嵌套验证 Validation<Validation<'c>>,因为最终函数本身 returns 验证。我想去掉其中一个验证,这样结果类型就是 Validation<'c>。我尝试使用我发现 here 的绑定和提升函数的变体进行试验,但结果保持不变。嵌套匹配是这里的唯一选项吗?

编辑 #1:这是我目前拥有的简化代码:

以下是处理验证的类型:

[<Struct>]
type Error = {
    Message: string
    Code: int
}
    
type Validation<'a> =
    | Success of 'a
    | Failure of Error list

let apply elevatedFunction elevatedValue =
    match elevatedFunction, elevatedValue with
    | Success func, Success value -> Success (func value)
    | Success _, Failure errors -> Failure errors
    | Failure errors, Success _ -> Failure errors
    | Failure currentErrors, Failure newErrors -> Failure (currentErrors@newErrors)

let (<*>) = apply

有问题的功能是这个:

let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport): Validation<Validation<string>> =
    Success formatReportAsText
    <*> languageTranslatorFor unvalidatedLanguageName
    <*> reportFrom unvalidatedReport

验证函数:

let languageTranslatorFor (unvalidatedLanguageName: string): Validation<Entry -> string> = ...

let reportFrom (unvalidatedReport: UnvalidatedReport): Validation<Report> = ...

使用验证参数的函数:

let formatReportAsText (languageTranslator: Entry -> string) (report: Report): Validation<string> = ...

编辑#2:我尝试使用@brianberns 提供的 并实现了验证<'a> 类型的计算表达式:

// Validation<'a> -> Validation<'b> -> Validation<'a * 'b>
let zip firstValidation secondValidation =
    match firstValidation, secondValidation with
    | Success firstValue, Success secondValue -> Success(firstValue, secondValue)
    | Failure errors, Success _ -> Failure errors
    | Success _, Failure errors -> Failure errors
    | Failure firstErrors, Failure secondErrors -> Failure (firstErrors @ secondErrors)

// Validation<'a> -> ('a -> 'b) -> Validation<'b>
let map elevatedValue func =
    match elevatedValue with
    | Success value -> Success(func value)
    | Failure validationErrors -> Failure validationErrors

type MergeValidationBuilder() =
    member _.BindReturn(validation: Validation<'a>, func) = Validation.map validation func
        
    member _.MergeSources(validation1, validation2) = Validation.zip validation1 validation2
    
let validate = MergeValidationBuilder()

并这样使用它:

let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport): Validation<Validation<string>> =
    validate = {
        let! translator = languageTranslatorFor unvalidatedLanguageName
        and! report = reportFrom unvalidatedReport

        return formatReportAsText translator report
    }

虽然计算表达式肯定更好读,但最终结果仍然完全相同 [Validation],因为“formatReportAsText”函数也 returns 结果包含在 Validation 中。 为了稍微合并堆叠验证,我使用了下面的函数,但对我来说似乎很笨重:

// Validation<Validation<'a>> -> Validation<'a>
let merge (nestedValidation: Validation<Validation<'a>>): Validation<'a> =
    match nestedValidation with
    | Success innerValidation ->
        match innerValidation with
        | Success value -> Success value
        | Failure innerErrors -> Failure innerErrors
    | Failure outerErrors -> Failure outerErrors

编辑 #3: 将“ReturnFrom”函数添加到验证计算表达式以展平嵌套验证后,验证函数按预期工作。

member _.ReturnFrom(validation) = validation

使用计算表达式的验证函数的最终版本是:

let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport): Validation<string> =
    validate = {
        let! translator = languageTranslatorFor unvalidatedLanguageName
        and! report = reportFrom unvalidatedReport

        return! formatReportAsText translator report
    }

首先,我认为您正在进入 F# 的一个相当高级的领域 - 但最实用的解决方案是使用在评论中@brianberns 链接的先前答案中引用的计算构建器。

如果您想坚持使用基于组合器的更简单的方法,可以使用以下函数来实现:

val merge : Validation<'a> -> Validation<'b> -> Validation<'a * 'b>
val bind : ('a -> Validation<'b>) -> Validation<'a> -> Validation<'b>

Merge 是一个函数,它采用两个可能经过验证的值并生成一个合并错误的新值(作为您的原始应用函数)。 Bind 函数将函数应用于经过验证的值并折叠结果中的“嵌套”验证。它们可以实现为:

let merge elevatedValue1 elevatedValue2 = 
    match elevatedValue1, elevatedValue2 with 
    | Success v1, Success v2 -> Success (v1, v2)
    | Success _, Failure errors -> Failure errors
    | Failure errors, Success _ -> Failure errors
    | Failure e1, Failure e2 -> Failure (e1 @ e2)

let bind f elevatedValue =
    match elevatedValue with
    | Success value -> 
        match f value with 
        | Success value -> Success value
        | Failure e -> Failure e
    | Failure e -> Failure e

感谢 merge,您可以验证两个输入并(可能)合并错误。感谢 bind,您可以继续计算并处理其余部分也可能失败的事实。您可以将组合函数编写为:

let formatReport (unvalidatedLanguageName: string) 
      (unvalidatedReport: UnvalidatedReport): Validation<string> =
  merge 
    (languageTranslatorFor unvalidatedLanguageName)
    (reportFrom unvalidatedReport)
  |> bind formatReportAsText 

有很多方法可以给猫蒙皮,但大多数方法的核心是每当遇到像 Validation<Validation<string>> 这样的嵌套容器时,您都需要一些方法来 'flatten' 嵌套.对于像 Validation 这样的类型,这很简单:

// Validation<Validation<'a>> -> Validation<'a>
let join = function
    | Success x -> x
    | Failure errors -> Failure errors

您也可以选择调用此函数 flatten,但它通常被称为 join

您可能还会发现 map 函数很有用。这个也很简单:

// ('a -> 'b) -> Validation<'a> -> Validation<'b>
let map f = function
    | Success x -> Success (f x)
    | Failure errors -> Failure errors

这样的map函数使得Validation成为functor.

当您同时拥有 mapjoin 时,您可以始终 实现通常称为 bind:[=35= 的方法]

// ('a -> Validation<'b>) -> Validation<'a> -> Validation<'b>
let bind f = map f >> join

扁平化或 join 嵌套容器的能力使其成为 monad。虽然它是一个充满神秘和敬畏的词,但它确实就是这样:它是一个可以展平的函子。

然而,通常 joinbind 是相反定义的:

let bind f = function
    | Success x -> f x
    | Failure errors -> Failure errors

let join x = bind id x

使用 join 您可以通过展平嵌套容器来调整有问题的功能:

// string -> UnvalidatedReport -> Validation<string>
let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport) =
    Success formatReportAsText
    <*> languageTranslatorFor unvalidatedLanguageName
    <*> reportFrom unvalidatedReport
    |> join

但是,我不会这样做。虽然这样的组合体操很有趣,但它们并不总是最易读的解决方案。

计算表达式

我更喜欢定义一个 Computation Expression,它至少可以像这样完成:

type ValidationBuilder () =
    member _.Bind (x, f) = bind f x
    member _.ReturnFrom x = x

let validate = ValidationBuilder ()

现在您可以像这样编写所需的函数:

// string -> UnvalidatedReport -> Validation<string>
let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport) =
    validate {
        let! l = languageTranslatorFor unvalidatedLanguageName
        let! r = reportFrom unvalidatedReport
        return! formatReportAsText l r
    }

然而,这个版本的问题在于它没有使用 apply 函数来一起追加错误。换句话说,它会在遇到第一个错误时短路。

为了支持在不短路的情况下收集错误,您将需要一个支持 applicative functors, like @brianberns pointed to. You can also see an example here.

的计算生成器

由于 formatReportAsText returns 是经过验证的字符串(而不是普通字符串),您应该在计算表达式的末尾使用 return! 而不是 return

return! formatReportAsText translator report

这相当于:

let! value = formatReportAsText translator report   // value is a string
return value

如果我正确理解你的代码,计算表达式的类型将是 Validation<string> 而不是 Validation<Validation<string>>

请注意,您需要在构建器上使用 ReturnFrom 方法才能使 return! 正常工作:

member __.ReturnFrom(value) = value

详情见this page