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?