2
votes

Given the result type

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

I have these functions which all return Async < Result<'t> > which are combined something like this:

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
    }

This code looks bad so instead I would like to have this:

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 

So the questions are:

  1. How do I write function c such that it returns an error in mymonad?
  2. How do I write function d such that it wraps an async with a mymonad?
  3. How can I make my monad parameterized in a similar way Async is?

...such that I can write

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

UPDATE:

Also I would like to run several mymonad operations in parallel and then look at the array of results to see what were the errors and the successes. For this reason I think using exceptions is not a good idea.

Also, regarding the question 3, what I meant was to have my type parameterized and opaque such that the callers don't know/don't care they are dealing with an async. The way I wrote the monad the caller can always use Async.RunSynchronously to run a mymonad expression.

UPDATE 2:

So far I ended up with the following:

  1. I use an explicit type for each member of MyMonadBuilder
  2. I added ReturnFrom to the MyMonadBuilder. I use this function to wrap an Async< Result<'t> >
  3. I added helper functions like failwith which create a mymonad with the error value

The code looks like this:

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

This looks reasonably good for my purpose. Thanks!

2

2 Answers

3
votes

The asyncChoice workflow from my ExtCore library already implements this -- it's available on NuGet so all you need to do is add a reference to your project, open the ExtCore.Control namespace in your source files, and start writing code like this:

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 is constructed using the standard Async<'T> and Choice<_,_> types from the F# Core library, so you shouldn't have any compatibility problems.

4
votes

Asynchronous workflows automatically support error handling through exceptions, so the idiomatic solution is to just use exceptions. If you want to distinguish some special kind of errors, then you can just define a custom exception type:

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 }

If you want to handle exceptions, you can use try ... with MyError msg -> ... inside an asynchronous workflow.

You could define a custom computation builder that re-implements this using an algebraic data type such as your Result, but unless you have some really good reason for doing that, I would not recommend this approach - it will not work with standard libraries, it is quite complicated and does not fit with the general F# style.

In your computation expression, the type of values is Async<Result<'T>>, return automatically wraps the argument of type 'T in an async workflow that returns Ok. If you wanted to construct a value representing a failure, you can use return! and create an async workflow that returns Result.Error. You probably need something like this:

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

But as I said, using exceptions is a better approach.

EDIT: To answer the question in the comments - if you have a number of async computations, you can still wrap the final result in your custom type. However, you do not need to rebuild the entire asynchronous workflow library - errors in the primitive operations can still be handled using standard exceptions:

// 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 }