【问题标题】:F# - Nested typesF# - 嵌套类型
【发布时间】:2021-11-10 13:29:42
【问题描述】:

我正在尝试使用 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&lt;Validation&lt;'c&gt;&gt;,因为最终函数本身会返回 Validation。我想摆脱验证之一,因此结果类型将是Validation&lt;'c&gt;。我尝试使用我发现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 提供的solution 并实现了Validation 类型的计算表达式:

// 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
    }

虽然计算表达式肯定更好地读取最终结果,但由于“formatReportAsText”函数也返回包装在验证中的结果,因此仍然完全相同 [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
    }

【问题讨论】:

  • 你能告诉我们你目前拥有的代码吗?
  • I consider validation a solved problem in FP,但如果(如@brianberns 请求)您分享minimal, reproducible example,我将很乐意为您提供具体帮助。
  • 根据您目前提供的信息,我认为SO answer 可能是您正在寻找的。它是一个计算表达式生成器,使用新的and! 语法来累积验证错误。 (如果您真的想序列化验证而不是并行执行,可以添加 Bind 方法。)
  • 你需要的是Result.bind

标签: validation functional-programming f#


【解决方案1】:

有很多方法可以给猫剥皮,但其中大多数方法的核心是,每当您遇到像 Validation&lt;Validation&lt;string&gt;&gt; 这样的嵌套容器时,您都需要一些方法来“压平”嵌套。对于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 的方法:

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

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

不过,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 的计算构建器,就像@brianberns 指出的那样。您还可以查看示例here

【讨论】:

  • 感谢您的广泛回答马克。 ReturnFrom 似乎是我缺少的部分。关于您写的内容,我有几个问题:内部和外部验证都失败并且加入会丢弃内部验证错误的情况是否可能?其次,单子。正如你所说,这是一个充满神秘色彩的词,我是否理解正确,它是一个旨在处理副作用的“容器”[例如 Async]?
  • @Bremewood 对于你的第一个问题,我认为你最好通过自己发现答案来学习:尝试创建一个 Validation&lt;Validation&lt;'a&gt; 值,正如你所说,内部和外部值都是Failure。你学什么?至于第二个问题,container 这个词为函子提供了一个心智模型,但是theoretically, it goes deeper than that。然而,它不一定是关于副作用的。例如,List 和 Maybe 不模拟副作用。
【解决方案2】:

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

return! formatReportAsText translator report

这相当于:

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

如果我正确理解您的代码,则计算表达式的类型将是 Validation&lt;string&gt; 而不是 Validation&lt;Validation&lt;string&gt;&gt;

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

member __.ReturnFrom(value) = value

详情请见this page

【讨论】:

  • 感谢您的解释。我似乎仍然很难正确理解计算表达式。添加 ReturnFrom 并使用 return 后!该功能完美运行。
  • 计算表达式功能强大,但可能令人费解。很高兴你的工作正常。如果您正在使用它,请接受答案。谢谢。
【解决方案3】:

首先,我认为您正在进入一个相当高级的 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 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-06
    • 1970-01-01
    • 1970-01-01
    • 2016-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多