2

给定结果类型

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

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

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< Result<'t>>
  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

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

4

2 回答 2

4

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

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 -> ...在异步工作流中使用。

可以定义一个自定义计算构建器,使用诸如 your 的代数数据类型重新实现它Result,但除非您有一些非常好的理由这样做,否则我不推荐这种方法 - 它不适用于标准库,它是相当复杂,不符合一般的 F# 风格。

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

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

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

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

// 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 }
于 2013-08-19T19:17:01.943 回答
3

asyncChoice我的ExtCore库中的工作流已经实现了这一点——它在 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<'T>Choice<_,_>类型构建的,因此您不应该有任何兼容性问题。

于 2013-08-20T00:40:10.090 回答