F# - 嵌套类型
F# - Nested types
我试图理解使用 F# 的函数式编程,为此我开始了一个小项目,但我 运行 在以下问题中似乎找不到任何优雅的解决方案。
我创建了 Validation<'a>
,它是非常专业的 F# 结果:Result<'a, Error list>
,它帮助我处理验证结果。
我有两个函数都使用签名执行一些验证:
'a -> Validation<'b>
还有第三个函数使用经过验证的带有签名的参数:
'a -> 'b -> Validation<'c>
我想实现的是:
- 验证参数'a
- 如果参数 'a 的验证通过,则验证参数 'b
- 如果参数 '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.
当您同时拥有 map
和 join
时,您可以始终 实现通常称为 bind
:[=35= 的方法]
// ('a -> Validation<'b>) -> Validation<'a> -> Validation<'b>
let bind f = map f >> join
扁平化或 join
嵌套容器的能力使其成为 monad。虽然它是一个充满神秘和敬畏的词,但它确实就是这样:它是一个可以展平的函子。
然而,通常 join
和 bind
是相反定义的:
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。
我试图理解使用 F# 的函数式编程,为此我开始了一个小项目,但我 运行 在以下问题中似乎找不到任何优雅的解决方案。
我创建了 Validation<'a>
,它是非常专业的 F# 结果:Result<'a, Error list>
,它帮助我处理验证结果。
我有两个函数都使用签名执行一些验证:
'a -> Validation<'b>
还有第三个函数使用经过验证的带有签名的参数:
'a -> 'b -> Validation<'c>
我想实现的是:
- 验证参数'a
- 如果参数 'a 的验证通过,则验证参数 'b
- 如果参数 '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 提供的
// 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
// 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.
当您同时拥有 map
和 join
时,您可以始终 实现通常称为 bind
:[=35= 的方法]
// ('a -> Validation<'b>) -> Validation<'a> -> Validation<'b>
let bind f = map f >> join
扁平化或 join
嵌套容器的能力使其成为 monad。虽然它是一个充满神秘和敬畏的词,但它确实就是这样:它是一个可以展平的函子。
然而,通常 join
和 bind
是相反定义的:
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。