2

我想为任一表达式构建一个计算表达式。这很简单

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!"]

但是在失败(多个)的情况下,我想累积这些并返回一个失败,如下所示。

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并始终返回来做到这一点有一个想法Success(尽管有一些额外的结构表示累积的错误)。但是,如果我这样做,那么我会错过停止信号并且我总是会返回返回值(实际上并不是真的,因为我会遇到运行时异常,但原则上)

4

4 回答 4

4

我认为你试图做的事情不能用单子来表达。问题是,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# 中有多种编码应用函子的方法,但没有很好的语法,因此您很可能最终会使用丑陋的自定义运算符。

于 2017-10-05T09:59:09.030 回答
1

最终,有了上面@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!"]
于 2017-10-05T10:34:01.183 回答
1

正如@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无论好坏,a 总是有一个值。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

输出:

结果 (1,Fork (Leaf (Message "Oh nose!"),Leaf (Message "God fucking it, uncle Bob!")))

那是; 我们得到一个不好的值1,它不好的原因是因为两个连接的错误叶子。

我在使用可组合组合器转换和验证树结构时使用了这种方法。在我的情况下,让所有验证失败都很重要,而不仅仅是第一次。这意味着Bind需要评估 in 的两个分支,但为了这样做,我们必须始终有一个值才能调用ufin Bind t uf

正如我在 OP:s own answer 中尝试过的一样,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
于 2017-10-06T11:09:31.423 回答
1

and!现在,我们在构建器中使用了和 MergeSources 的应用计算表达式。

请参阅以获取解决方案

于 2021-02-24T09:06:49.677 回答