【发布时间】:2021-11-10 13:29:42
【问题描述】:
我正在尝试使用 F# 来理解函数式编程,为此我开始了一个小项目,但我遇到了以下问题,似乎找不到任何优雅的解决方案。
我创建了Validation<'a>,它是非常专业的 F# 结果:Result<'a, Error list>,它可以帮助我处理验证结果。
我有两个函数都使用签名执行一些验证:
'a -> Validation<'b>
还有第三个函数使用经过验证的带有签名的参数:
'a -> 'b -> Validation<'c>
我想要实现的是:
- 验证参数 'a
- 如果参数 'a 的验证通过,验证参数 'b
- 如果参数 'b 的验证通过,则将参数 'a 和 'b 提供给最终函数
到目前为止,我使用 apply 函数来实现这种行为,但是当我在这种情况下尝试使用它时,结果类型是嵌套的 Validation Validation<Validation<'c>>,因为最终函数本身会返回 Validation。我想摆脱验证之一,因此结果类型将是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 提供的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#