3
\$\begingroup\$

In F# the Railway Oriented Programming pattern can be implemented synchronously using the Result<'t,'terr> type as result value for functions, so they can be foreward piped like:

let result = 
    func1
    |> Result.bind func2
    |> Result.bind func3
    |> Result.bind func4

I would like to implement the same pattern for functions returning Async<Result<'t,'terr>>, so I can pipe them like

let resultAsync = 
    func1Async
    |> Result.bindAsync func2Async
    |> Result.bindAsync func3Async
    |> Result.bindAsync func4Async

and execute that in a non blocking manner like

let result = resultAsync |> Async.RunSynchronously

match result with
| Ok(value) -> todoHandleOk values
| Error(msg) -> todoHandleError msg

To accomplish that I have implemented Result.bindAsync as an extension:

module Result = 
    let bindAsync<'t,'s,'terr> (binder:'t -> Async<Result<'s,'terr>>) (result:Async<Result<'t,'terr>>) : Async<Result<'s,'terr>> = 
        async {
            let! res = result
            match res with
            | Ok(value) -> return! binder value
            | Error(err) -> return Error(err)
        }

As shown it mimic the "normal" bind operation for a computational expression but encapsulated in a async workflow expression and hence returning an Async<Result<'s,'terr>>. The result argument is the Async "result" from the previous function which is executed to get its materialized result (res) before it can be used either as argument to the binder which is the next step on the successful track or returned as an error if unsuccessful.


Test Case

The below is a console test case with some functions that can be bound using Result.bindAsync as shown above.

module AsyncBindTests =

let anotherProcAsync = 
    async {
        for i in 1..Int32.MaxValue do
            printfn "Another Proc is at: %d" i
            do! Async.Sleep 800
    }

let rand = new Random();

let collectValuesAsync =
    async {
        printfn "Collecting Values..."
        let count = rand.Next(10, 15)
        let values = 
            List.init count (
                fun i -> 
                    async {
                        let x = rand.Next(1, 1000)
                        printfn "Value Collected: %d" x
                        do! Async.Sleep 500
                        return x
                    } |> Async.RunSynchronously
            )
        printfn "Values Collected"
        match (values |> List.sum) % 3 with
        | 0 -> return Error("Values sum is a multiple of 3")
        | _ -> return Ok(values)
    }

let groupValuesAsync values = 
    async {
        printfn "Grouping Values..."
        let sizeSum = 
            values 
            |> List.groupBy (fun v -> v % 3)
            |> List.sumBy (fun (k, lst) -> lst |> List.length)
        do! Async.Sleep 2000
        match sizeSum % 3 with
        | 0 -> return Error("Sum of Group Sizes is a multiple of 3")
        | _ -> return Ok (sizeSum, values)
    }

let crossSum value = 
    let rec cs v sum = 
        if v = 0 then sum
        else cs (v / 10) (sum + (v % 10))
    cs value 0

let getCrossSumAsync (sizeSum, values) =
    async {
        printfn "Checking Cross Sum..."
        let result = values |> List.fold (fun csum value -> csum + (crossSum value)) (crossSum sizeSum)
        do! Async.Sleep 2000
        match result % 3 with
        | 0 -> return Error "Crosssum is a multiple of 3"
        | _ -> return Ok(values)
    }

let runValuesFunctions = 
    collectValuesAsync
    |> Result.bindAsync groupValuesAsync
    |> Result.bindAsync getCrossSumAsync


let run () =
    printfn "AsyncBindTests"

    Async.Start anotherProcAsync
    let result = runValuesFunctions |> Async.RunSynchronously

    match result with
    | Ok(values) -> values |> List.iter (printfn "Value: %d")
    | Error(msg) -> printfn "ERROR: %s" msg

    Console.ReadLine() |> ignore
    Async.CancelDefaultToken()

Any feedback on the use of Async/async are welcome, but I'm especially interested in your thoughts about the Result.bindAsync. Am I violating something or are there better ways to do it?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.