如何构建一个累积生成器 [英] How to Build an Accumulating Either Builder

查看:46
本文介绍了如何构建一个累积生成器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I want to build an computational expression for either expressions. That is simple enough

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

但是对于失败(多个),我想累加并返回如下失败.

But in the case of Failures (multiple) I want to accumulate those and return an Failure as below.

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 (尽管有一些表示累积错误的附加结构)的方法的想法.但是,如果执行此操作,则我会丢失停止信号,并且我总是会返回返回值(实际上并不是真的,因为我会遇到运行时异常,但原则上如此)

I have an idea on how to do that by rewriting Bind and always returning Success (albeit with some additional structure that signifies the accumulated erors). However if I do this then I am missing the stop signal and I always get back the return value (actually not really as I will run into a runtime exception, but in principle)

推荐答案

@tomasp所说的一种方法是,除了失败之外,还总是提供一个值,以使 bind 正常工作.这是我在处理此主题时一直使用的方法.然后,我将 Result 的定义更改为例如:

As @tomasp is saying one approach is to always provide a value in addition to the failures in order to make bind work properly. This is the approach I have been using when dealing with this subject. I would then change the definition of Result to, for example, this:

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 为空,则此值为好.

This means that a Result always has a value whether it's good or bad. The value is good iff the BadTree is empty.

我更喜欢树而不是列表的原因是 Bind 会聚合两个单独的结果,这些结果可能会有导致列表串联的子故障.

The reason I prefer trees over list is that Bind will aggregate two separate results that may have subfailures leading to list concatenations.

一些使我们创造好值或坏值的函数:

Some functions that let us create either good or bad value:

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 的类型,我们可以创建一种便捷方法:

Because even bad results need to carry a value in order to make Bind work we need to provide the value through bv parameter. For types that support Zero we can create a convinience method:

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)

那是;我们评估这两个结果,并在需要时加入坏树.

That is; we evaluate both results and join the bad trees if needed.

使用计算表达式生成器创建以下程序:

With a computation expression builder the following program:

  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,叉子(叶子(消息哦,鼻子!"),叶子(消息该死,鲍勃叔叔!"))))

Result (1,Fork (Leaf (Message "Oh nose!"),Leaf (Message "God damn it, uncle Bob!")))

那是;我们得到一个错误的值 1 ,并且之所以不好,是因为两个连接的错误叶子.

That is; we get a bad value 1 and the reasons it's bad is because of the two joined error leafs.

在使用可组合组合器转换和验证树结构时,我使用了这种方法.就我而言,重要的是要获得所有的验证失败,而不仅仅是第一个.这意味着需要对 Bind 中的两个分支进行评估,但是为了做到这一点,我们必须始终具有一个值,以便在 Bind t uf 中调用 uf .代码>.

I have used this approach when transforming and validating tree structures using composable combinators. It's in my case important to get all validation failure, not just the first. This means that both branches in Bind needs to be evaluated but in order to do we must always have a value in order to call uf in Bind t uf.

就像在OP中一样:我确实尝试了 Unchecked.defaultof< _> ,但由于例如字符串的默认值为 null ,所以我放弃了它通常会在调用 uf 时导致崩溃.我确实创建了一个地图 Type->空值,但在最终解决方案中,当构造不良结果时,我需要一个不良值.

As in OP:s own answer I did experiment with Unchecked.defaultof<_> but I abandoned for example because the default value of a string is null and it usually leads to crashes when invoking uf. I did create a map Type -> empty value but in my final solution I require a bad value when constructing a bad 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

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

这篇关于如何构建一个累积生成器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆