如何构建一个累积的 Either Builder
How to Build an Accumulating Either Builder
我想为任一表达式构建一个计算表达式。
够简单了
type Result<'TSuccess> =
| Success of 'TSuccess
| Failure of List<string>
type Foo = {
a: int
b: string
c: bool
}
type EitherBuilder () =
member this.Bind(x, f) =
match x with
| Success s -> f s
| Failure f -> Failure f
member this.Return x = Success x
let either = EitherBuilder ()
let Ok = either {
let! a = Success 1
let! b = Success "foo"
let! c = Success true
return
{
a = a
b = b
c = c
}
}
let fail1 = either {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Success true
return
{
a = a
b = b
c = c
}
} //returns fail1 = Failure ["Oh nose!"]
但在失败(多次)的情况下,我想累积这些和 return 如下所示的失败。
let fail2 = either {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Failure ["God damn it, uncle Bob!"]
return
{
a = a
b = b
c = c
}
} //should return fail2 = Failure ["Oh nose!"; "God damn it, uncle Bob!"]
我有一个关于如何通过重写 Bind
并始终 returning Success
来做到这一点的想法(尽管有一些额外的结构表示累积的错误)。但是,如果我这样做,那么我会错过停止信号,并且我总是会返回 return 值(实际上不是真的,因为我会 运行 进入 运行time 异常,但原则上)
我认为你试图做的事情不能用 monad 来表达。问题是 Bind
只能调用剩余的计算(这可能会产生更多的失败),如果它可以获得函数参数的值。在您的示例中:
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Failure ["God damn it, uncle Bob!"]
绑定无法调用以 b
开头的延续,因为 Failure ["Oh nose!"]
没有为 b
提供值。您可以使用默认值并保留错误,但这会改变您正在使用的结构:
type Result<'T> = { Value : 'T; Errors : list<string> }
您可以在需要的地方使用应用函子抽象来编写此代码:
Merge : F<'T1> * F<'T2> -> F<'T1 * 'T2>
Map : ('T1 -> 'T2) -> M<'T1> -> M<'T2>
Return : 'T -> M<'T>
您可以通过 Merge
累积错误(如果两个参数都表示失败)并且 Map
仅在没有值时应用计算的方式来实现所有这些。
在 F# 中有多种编码应用仿函数的方法,但没有合适的语法,因此您很可能最终会使用丑陋的自定义运算符。
最终,根据上面@tomas 的提示,我可以使用这个解决方案,它保留数据类型,但创建一个有状态的构建器。
现在唯一留给我的问题是这个线程安全 - 我想是的。也许有人可以确认?
type Result<'TSuccess> =
| Success of 'TSuccess
| Failure of List<string>
type Foo = {
a: int
b: string
c: bool
}
type EitherBuilder (msg) =
let mutable errors = [msg]
member this.Bind(x, fn) =
match x with
| Success s -> fn s
| Failure f ->
errors <- List.concat [errors;f]
fn (Unchecked.defaultof<_>)
member this.Return x =
if List.length errors = 1 then
Success x
else
Failure errors
let either msg = EitherBuilder (msg)
let Ok = either("OK") {
let! a = Success 1
let! b = Success "foo"
let! c = Success true
return
{
a = a
b = b
c = c
}
}
let fail1 = either("Fail1") {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Success true
return
{
a = a
b = b
c = c
}
} //returns fail1 = Failure ["Fail1"; "Oh nose!"]
let fail2 = either("Fail2") {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Failure ["God damn it, uncle Bob!"]
return
{
a = a
b = b
c = c
}
} //should return fail2 = Failure ["Fail2"; "Oh nose!"; "God damn it, uncle Bob!"]
正如@tomasp 所说,一种方法是始终在失败之外提供一个值,以使 bind
正常工作。这是我在处理这个主题时一直使用的方法。然后我会将 Result
的定义更改为,例如:
type BadCause =
| Exception of exn
| Message of string
type BadTree =
| Empty
| Leaf of BadCause
| Fork of BadTree*BadTree
type [<Struct>] Result<'T> = Result of 'T*BadTree
这意味着 Result
总是有一个值,无论它是好是坏。如果 BadTree
为空,则该值很好。
我更喜欢树而不是列表的原因是 Bind
将聚合两个单独的结果,这些结果可能有导致列表连接的子故障。
一些让我们创造好的或坏的价值的功能:
let rreturn v = Result (v, Empty)
let rbad bv bt = Result (bv, bt)
let rfailwith bv msg = rbad bv (Message msg |> Leaf)
因为即使是糟糕的结果也需要携带一个值才能使 Bind
工作,我们需要通过 bv
参数提供该值。对于支持 Zero
的类型,我们可以创建一个方便的方法:
let inline rfailwithz msg = rfailwith LanguagePrimitives.GenericZero<_> msg
Bind
容易实现:
let rbind (Result (tv, tbt)) uf =
let (Result (uv, ubt)) = uf tv
Result (uv, btjoin tbt ubt)
也就是说;我们评估两个结果并在需要时加入坏树。
使用计算表达式生成器的以下程序:
let r =
result {
let! a = rreturn 1
let! b = rfailwithz "Oh nose!"
let! c = rfailwithz "God damn it, uncle Bob!"
return a + b + c
}
printfn "%A" r
输出:
Result (1,Fork (Leaf (Message "Oh nose!"),Leaf (Message "God damn it, uncle Bob!")))
也就是说;我们得到了一个错误的值 1
,它之所以不好是因为两个连接的错误叶子。
我在使用可组合组合器转换和验证树结构时使用了这种方法。就我而言,重要的是让所有验证失败,而不仅仅是第一个。这意味着 Bind
中的两个分支都需要评估,但为了做到这一点,我们必须始终有一个值才能在 Bind t uf
.
中调用 uf
与 OP:s 自己的答案一样,我确实用 Unchecked.defaultof<_>
进行了实验,但我放弃了,例如,因为字符串的默认值为 null
并且它通常会在调用时导致崩溃 uf
。我确实创建了一个地图 Type -> empty value
但在我的最终解决方案中,我在构建错误结果时需要一个错误的值。
希望对您有所帮助
完整示例:
type BadCause =
| Exception of exn
| Message of string
type BadTree =
| Empty
| Leaf of BadCause
| Fork of BadTree*BadTree
type [<Struct>] Result<'T> = Result of 'T*BadTree
let (|Good|Bad|) (Result (v, bt)) =
let ra = ResizeArray 16
let rec loop bt =
match bt with
| Empty -> ()
| Leaf bc -> ra.Add bc |> ignore
| Fork (l, r) -> loop l; loop r
loop bt
if ra.Count = 0 then
Good v
else
Bad (ra.ToArray ())
module Result =
let btjoin l r =
match l, r with
| Empty , _ -> r
| _ , Empty -> l
| _ , _ -> Fork (l, r)
let rreturn v = Result (v, Empty)
let rbad bv bt = Result (bv, bt)
let rfailwith bv msg = rbad bv (Message msg |> Leaf)
let inline rfailwithz msg = rfailwith LanguagePrimitives.GenericZero<_> msg
let rbind (Result (tv, tbt)) uf =
let (Result (uv, ubt)) = uf tv
Result (uv, btjoin tbt ubt)
type ResultBuilder () =
member x.Bind (t, uf) = rbind t uf
member x.Return v = rreturn v
member x.ReturnFrom r = r : Result<_>
let result = Result.ResultBuilder ()
open Result
[<EntryPoint>]
let main argv =
let r =
result {
let! a = rreturn 1
let! b = rfailwithz "Oh nose!"
let! c = rfailwithz "God damn it, uncle Bob!"
return a + b + c
}
match r with
| Good v -> printfn "Good: %A" v
| Bad es -> printfn "Bad: %A" es
0
我们现在在构建器中有了 and!
和 MergeSources 的应用计算表达式。
参见 解决方案
我想为任一表达式构建一个计算表达式。 够简单了
type Result<'TSuccess> =
| Success of 'TSuccess
| Failure of List<string>
type Foo = {
a: int
b: string
c: bool
}
type EitherBuilder () =
member this.Bind(x, f) =
match x with
| Success s -> f s
| Failure f -> Failure f
member this.Return x = Success x
let either = EitherBuilder ()
let Ok = either {
let! a = Success 1
let! b = Success "foo"
let! c = Success true
return
{
a = a
b = b
c = c
}
}
let fail1 = either {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Success true
return
{
a = a
b = b
c = c
}
} //returns fail1 = Failure ["Oh nose!"]
但在失败(多次)的情况下,我想累积这些和 return 如下所示的失败。
let fail2 = either {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Failure ["God damn it, uncle Bob!"]
return
{
a = a
b = b
c = c
}
} //should return fail2 = Failure ["Oh nose!"; "God damn it, uncle Bob!"]
我有一个关于如何通过重写 Bind
并始终 returning Success
来做到这一点的想法(尽管有一些额外的结构表示累积的错误)。但是,如果我这样做,那么我会错过停止信号,并且我总是会返回 return 值(实际上不是真的,因为我会 运行 进入 运行time 异常,但原则上)
我认为你试图做的事情不能用 monad 来表达。问题是 Bind
只能调用剩余的计算(这可能会产生更多的失败),如果它可以获得函数参数的值。在您的示例中:
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Failure ["God damn it, uncle Bob!"]
绑定无法调用以 b
开头的延续,因为 Failure ["Oh nose!"]
没有为 b
提供值。您可以使用默认值并保留错误,但这会改变您正在使用的结构:
type Result<'T> = { Value : 'T; Errors : list<string> }
您可以在需要的地方使用应用函子抽象来编写此代码:
Merge : F<'T1> * F<'T2> -> F<'T1 * 'T2>
Map : ('T1 -> 'T2) -> M<'T1> -> M<'T2>
Return : 'T -> M<'T>
您可以通过 Merge
累积错误(如果两个参数都表示失败)并且 Map
仅在没有值时应用计算的方式来实现所有这些。
在 F# 中有多种编码应用仿函数的方法,但没有合适的语法,因此您很可能最终会使用丑陋的自定义运算符。
最终,根据上面@tomas 的提示,我可以使用这个解决方案,它保留数据类型,但创建一个有状态的构建器。
现在唯一留给我的问题是这个线程安全 - 我想是的。也许有人可以确认?
type Result<'TSuccess> =
| Success of 'TSuccess
| Failure of List<string>
type Foo = {
a: int
b: string
c: bool
}
type EitherBuilder (msg) =
let mutable errors = [msg]
member this.Bind(x, fn) =
match x with
| Success s -> fn s
| Failure f ->
errors <- List.concat [errors;f]
fn (Unchecked.defaultof<_>)
member this.Return x =
if List.length errors = 1 then
Success x
else
Failure errors
let either msg = EitherBuilder (msg)
let Ok = either("OK") {
let! a = Success 1
let! b = Success "foo"
let! c = Success true
return
{
a = a
b = b
c = c
}
}
let fail1 = either("Fail1") {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Success true
return
{
a = a
b = b
c = c
}
} //returns fail1 = Failure ["Fail1"; "Oh nose!"]
let fail2 = either("Fail2") {
let! a = Success 1
let! b = Failure ["Oh nose!"]
let! c = Failure ["God damn it, uncle Bob!"]
return
{
a = a
b = b
c = c
}
} //should return fail2 = Failure ["Fail2"; "Oh nose!"; "God damn it, uncle Bob!"]
正如@tomasp 所说,一种方法是始终在失败之外提供一个值,以使 bind
正常工作。这是我在处理这个主题时一直使用的方法。然后我会将 Result
的定义更改为,例如:
type BadCause =
| Exception of exn
| Message of string
type BadTree =
| Empty
| Leaf of BadCause
| Fork of BadTree*BadTree
type [<Struct>] Result<'T> = Result of 'T*BadTree
这意味着 Result
总是有一个值,无论它是好是坏。如果 BadTree
为空,则该值很好。
我更喜欢树而不是列表的原因是 Bind
将聚合两个单独的结果,这些结果可能有导致列表连接的子故障。
一些让我们创造好的或坏的价值的功能:
let rreturn v = Result (v, Empty)
let rbad bv bt = Result (bv, bt)
let rfailwith bv msg = rbad bv (Message msg |> Leaf)
因为即使是糟糕的结果也需要携带一个值才能使 Bind
工作,我们需要通过 bv
参数提供该值。对于支持 Zero
的类型,我们可以创建一个方便的方法:
let inline rfailwithz msg = rfailwith LanguagePrimitives.GenericZero<_> msg
Bind
容易实现:
let rbind (Result (tv, tbt)) uf =
let (Result (uv, ubt)) = uf tv
Result (uv, btjoin tbt ubt)
也就是说;我们评估两个结果并在需要时加入坏树。
使用计算表达式生成器的以下程序:
let r =
result {
let! a = rreturn 1
let! b = rfailwithz "Oh nose!"
let! c = rfailwithz "God damn it, uncle Bob!"
return a + b + c
}
printfn "%A" r
输出:
Result (1,Fork (Leaf (Message "Oh nose!"),Leaf (Message "God damn it, uncle Bob!")))
也就是说;我们得到了一个错误的值 1
,它之所以不好是因为两个连接的错误叶子。
我在使用可组合组合器转换和验证树结构时使用了这种方法。就我而言,重要的是让所有验证失败,而不仅仅是第一个。这意味着 Bind
中的两个分支都需要评估,但为了做到这一点,我们必须始终有一个值才能在 Bind t uf
.
uf
与 OP:s 自己的答案一样,我确实用 Unchecked.defaultof<_>
进行了实验,但我放弃了,例如,因为字符串的默认值为 null
并且它通常会在调用时导致崩溃 uf
。我确实创建了一个地图 Type -> empty value
但在我的最终解决方案中,我在构建错误结果时需要一个错误的值。
希望对您有所帮助
完整示例:
type BadCause =
| Exception of exn
| Message of string
type BadTree =
| Empty
| Leaf of BadCause
| Fork of BadTree*BadTree
type [<Struct>] Result<'T> = Result of 'T*BadTree
let (|Good|Bad|) (Result (v, bt)) =
let ra = ResizeArray 16
let rec loop bt =
match bt with
| Empty -> ()
| Leaf bc -> ra.Add bc |> ignore
| Fork (l, r) -> loop l; loop r
loop bt
if ra.Count = 0 then
Good v
else
Bad (ra.ToArray ())
module Result =
let btjoin l r =
match l, r with
| Empty , _ -> r
| _ , Empty -> l
| _ , _ -> Fork (l, r)
let rreturn v = Result (v, Empty)
let rbad bv bt = Result (bv, bt)
let rfailwith bv msg = rbad bv (Message msg |> Leaf)
let inline rfailwithz msg = rfailwith LanguagePrimitives.GenericZero<_> msg
let rbind (Result (tv, tbt)) uf =
let (Result (uv, ubt)) = uf tv
Result (uv, btjoin tbt ubt)
type ResultBuilder () =
member x.Bind (t, uf) = rbind t uf
member x.Return v = rreturn v
member x.ReturnFrom r = r : Result<_>
let result = Result.ResultBuilder ()
open Result
[<EntryPoint>]
let main argv =
let r =
result {
let! a = rreturn 1
let! b = rfailwithz "Oh nose!"
let! c = rfailwithz "God damn it, uncle Bob!"
return a + b + c
}
match r with
| Good v -> printfn "Good: %A" v
| Bad es -> printfn "Bad: %A" es
0
我们现在在构建器中有了 and!
和 MergeSources 的应用计算表达式。
参见