【问题标题】:F# custom computation workflowF# 自定义计算工作流
【发布时间】:2013-08-19 17:08:20
【问题描述】:

给定结果类型

type Result<'t> = OK of 't | Error of string 

我有这些函数,它们都返回 Async >,它们的组合如下:

let a = async { return Result.OK 1000 }
let b = async { return Result.Error "some message" }

let sum x y = 
    async {
        let! r1 = x
        match r1 with
        | Result.OK v1 -> 
             let! r2 = y
             match r2 with
             | Result.OK v2 -> return v1 + v2
             | Result.Error msg -> return Result.Error msg
        | Result.Error msg -> return Result.Error msg
    }

这段代码看起来很糟糕,所以我想要这个:

type Result = Ok of int | Error of string

type MyMonadBuilder() =
    member x.Bind (v,f) = 
        async { 
            let! r = v
            match r with
            | Ok r' -> return! f r'
            | Error msg -> return Error msg
        }

    member x.Return v = async {return Ok v }

    member x.Delay(f) = f()

let mymonad = MyMonadBuilder()
let runMyMonad = Async.RunSynchronously

let a = mymonad { return 10 }
let b = mymonad { return 20 }

let c = 
    mymonad { 
        return Result.Error "Some message"
        //??? The above doesn't work but how do I return a failure here?
    }

let d = 
    async {
        return Ok 1000
    } 
    //how to wrap this async with mymonad such that I can use it together with my other computation expressions?

let sum x y = 
    mymonad {
        let! v1 = x
        let! v2 = y
        return v1 + v2
    }

[<EntryPoint>]
let main argv = 
    let v = sum a b |> runMyMonad
    match v with
    | Ok v' -> printfn "Ok: %A" v'
    | Error msg -> printf "Error: %s" msg

    System.Console.Read() |> ignore
    0 

所以问题是:

  1. 如何编写函数 c 以使其在 mymonad 中返回错误?
  2. 如何编写函数 d 以便用 mymonad 包装异步?
  3. 如何使我的 monad 以与 Async 类似的方式参数化?

...这样我就可以写了

let f (a:MyMonad<int>) (b:MyMonad<string>) = ...

更新:

我还想并行运行几个 mymonad 操作,然后查看结果数组以查看错误和成功之处。出于这个原因,我认为使用异常不是一个好主意。

另外,关于问题 3,我的意思是让我的类型参数化和不透明,以便调用者不知道/不在乎他们正在处理异步。我编写 monad 的方式,调用者总是可以使用 Async.RunSynchronously 来运行 mymonad 表达式。

更新 2:

到目前为止,我得到了以下结果:

  1. 我为 MyMonadBuilder 的每个成员使用显式类型
  2. 我将 ReturnFrom 添加到 MyMonadBuilder。我使用这个函数来包装一个 Async >
  3. 我添加了一些辅助函数,例如 failwith,它创建了一个带有错误值的 mymonad

代码如下所示:

type MyMonad<'t> = 't Result Async

type MyMonadBuilder() =
    member x.Bind<'t> (v,f) : MyMonad<'t>= 
        async { 
            let! r = v
            match r with
            | Ok r' -> return! f r'
            | Error msg -> return Error msg
        }

    member x.Return<'t> v  : MyMonad<'t> = async {return Ok v }
    member x.ReturnFrom<'t> v  : MyMonad<'t> = v

    member x.Delay(f) = f()

let failwith<'t> : string -> MyMonad<'t> = Result.Error >> async.Return

这对于我的目的来说看起来相当不错。谢谢!

【问题讨论】:

    标签: f# monads monad-transformers


    【解决方案1】:

    异步工作流通过异常自动支持错误处理,因此惯用的解决方案是只使用异常。如果你想区分一些特殊类型的错误,那么你可以定义一个自定义的异常类型:

    exception MyError of string
    
    // Workflow succeeds and returns 1000
    let a = async { return 1000 }
    // Workflow throws 'MyError' exception
    // (using return! means that it can be treated as a workflow returning int)
    let b = async { return! raise (MyError "some message") }
    
    // Exceptions are automatically propagated
    let sum = async {
      let! r1 = a
      let! r2 = b
      return r1 + r2 }
    

    如果要处理异常,可以在异步工作流中使用try ... with MyError msg -&gt; ...

    可以定义一个自定义计算构建器,该构建器使用代数数据类型(例如 Result)重新实现这一点,但除非您有充分的理由这样做,否则我不建议您这样做这种方法 - 它不适用于标准库,它非常复杂并且不适合一般的 F# 样式。

    在您的计算表达式中,值的类型为Async&lt;Result&lt;'T&gt;&gt;return 自动将'T 类型的参数包装在返回Ok 的异步工作流中。如果您想构造一个表示失败的值,您可以使用return! 并创建一个返回Result.Error 的异步工作流。你可能需要这样的东西:

    let c = mymonad { 
      return! async.Return(Result.Error "Some message")
    }
    let d = mymonad {
        return 1000
    } 
    

    但正如我所说,使用异常是一种更好的方法。

    编辑:要回答 cmets 中的问题 - 如果您有许多异步计算,您仍然可以将最终结果包装在您的自定义类型中。但是,您不需要重新构建整个异步工作流库 - 原始操作中的错误仍然可以使用标准异常来处理:

    // Primitive async work that may throw an exception
    let primitiveAsyncWork = async { ... } 
    
    // A wrapped computation that returns standard Option type
    let safeWork = async {
      try 
        let! res = primitiveAsyncWork
        return Some res
      with e -> return None }
    
    // Run 10 instances of safeWork in parallel and filter out failed computations
    async { let! results = [ for i in 0 .. 9 -> safeWork ] |> Async.Parallel
            return results |> Seq.choose id }
    

    【讨论】:

    • 感谢您的回答。我理解您关于使用异常的观点。我的问题是我想并行运行许多这些 mymonad 操作。他们中的一些人可能会失败,他们中的一些人可能会成功。最后,我想查看结果数组并过滤错误或成功。出于这个原因,使用 Result 比使用异常更好,因为它更“并行友好”。
    • @vidi 您不需要为此定义自定义工作流程。将计算包装在 try .. with 中并在主体中返回 Ok res 并在异常处理程序中返回 Error ex 就足够了。
    • 假设我想并行运行多个计算。这给了我一个结果列表。然后对于列表中的每个项目,根据结果类型(即 OK | Error 等),我重试其中一些 - 这取决于。重试决定是在上层做出的,我不希望低级异步计算本身知道什么时候应该重试。如果我基于异常实现这一点并且列表中的一个计算失败,那么整个组合计算都会失败,这不是我想要的
    • @vidi 我添加了一个示例来显示答案。我认为它非常简单明了 - 您可以将该功能包装在一些可重用的功能中,它肯定比使用自定义工作流程更干净......
    • 要记住的另一件事是,如果您选择定义自己的自定义工作流程(甚至使用 CoreExt asyncChoice),那么大多数标准功能将无法直接使用 - 您将拥有为所有内容编写包装器。
    【解决方案2】:

    我的ExtCore 库中的asyncChoice 工作流已经实现了这一点——它在 NuGet 上可用,所以您只需添加对项目的引用,在源文件中打开 ExtCore.Control 命名空间,然后开始写这样的代码:

    open ExtCore.Control
    
    let asyncDivide100By (x : int) =
        asyncChoice {
            if x = 0 then
                return! AsyncChoice.error "Cannot divide by zero."
            else
                return (100 / x)
        }
    
    let divide100By (x : int) =
        let result =
            asyncDivide100By x
            |> Async.RunSynchronously
    
        match result with
        | Choice1Of2 result ->
            printfn "100 / %i = %i" x result
        | Choice2Of2 errorMsg ->
            printfn "An error occurred: %s" errorMsg
    
    
    [<EntryPoint>]
    let main argv =
        divide100By 10
        divide100By 1
        divide100By 0
    
        0   // Exit code
    

    asyncChoice 是使用 F# Core 库中的标准 Async&lt;'T&gt;Choice&lt;_,_&gt; 类型构造的,因此您不应该遇到任何兼容性问题。

    【讨论】:

    • 我认为广泛使用 Choice 不会产生可读的代码。我似乎无法记住 Choice1 和 Choice2 的含义。因此,如果在整个库中使用结果类型,我认为最好定义一个自定义类型。除了 Ok 和 Error 之外,可能还有其他 Result 构造函数(例如:Canceled)。另一方面,我会看看你的代码,因为有一些很好的例子,代码看起来干净易读。谢谢
    • 我将此标记为已接受的答案,因为 Extcore 中的代码提供了了解我需要做什么的指导。谢谢!
    • @vidi ExtCore 还提供了一个活动模式(|Success|Error|),以便在您使用Choice&lt;_,_&gt; 值时方便。它是内联的,因此使用它不会有任何运行时性能损失。
    • 文档仍然不完整:\
    猜你喜欢
    • 1970-01-01
    • 2016-05-17
    • 1970-01-01
    • 2013-05-18
    • 1970-01-01
    • 1970-01-01
    • 2013-02-04
    • 2018-12-13
    • 2019-10-28
    相关资源
    最近更新 更多