I have a sample railway pipeline that works well:
open FSharpPlus
let funA n =
if n < 10 then Ok n
else Error "not less than 10"
let funB n =
if n < 5 then Ok (n, n * 2)
else Error "not less than 5"
let funC n = // int -> Result<(int * int), string>
n
|> funA
>>= funB // it works
But when I want to turn the funB
to an async function, I got a compilation error. Logically it shouldn't be different. Same output/input... What's wrong?
What should be done to make it work?!
open FSharpPlus
let funA n =
if n < 10 then Ok n
else Error "not less than 10"
let funB n = async {
if n < 5 then return Ok (n, n * 2)
else return Error "not less than 5" }
let funC n = // int -> Async<Result<(int * int), string>>
n
|> funA
>>= funB // compile error
Same output/input... What's wrong?
No, they don't have the same output/input.
If you look at the type of (>>=)
it's something like 'Monad<'T> -> ('T -> 'Monad<'U>) -> 'Monad<'U>
which is a fake signature of a generic bind operation, overloaded for Monads in general. In your first example the Monad is Result<_,'TError>
, so your first example could be re-written as:
let funC n = // int -> Result<(int * int), string>
n
|> funA
|> Result.bind funB
The signature of Result.bind
is ('T -> Result<'U,'TError>) -> Result<'T,'TError> -> Result<'U,'TError>
. This makes sense, if you think about it. It's like applying the substitution Monad<_>
with Result<_,'TError>
and you have the arguments flipped that's why we use |>
.
Then your functions are both int -> Result<_,'TError>
so types match, and it makes sense (and it works).
Now, moving to your second code fragment, the function funB
has a different signature, it has Async<Result<_,'TError>>
so now types don't match. And it also makes sense, you can't use the bind implementation of Result
for an Async
.
So, what's the solution?
The easiest solution is don't use bind, at least not for 2 monads. You can "lift" your first function to Async
and use async.Bind
, with the generic >>=
or a standard async workflow, but inside it you'll have to use a manual match
to bind the results to the second function.
The other approach is more interesting, but also more complex to understand, it consists in using an abstraction called Monad Transformers:
open FSharpPlus.Data
let funC n = // int -> Result<(int * int), string>
n
|> (funA >> async.Return >> ResultT)
>>= (funB >> ResultT)
|> ResultT.run
So, what we do here is we "lift" the funA
function to Async
, then we wrap it in ResultT
which is a monad transformer for Result
, so it has a bind operation that takes care of binding on the outer monad as well, in our case Async
.
Then we simply wrap funB
into ResultT
and at the very end of the function we unwrap from ResultT
with Result.run
.
For more examples on layered monads in F#, see these questions
There are other approaches, some libraries provide some "magic workflows" which uses ad-hoc overloading to combine monads with composed monads (aka layered monads), so you write less code, but it's not as easy to reason about the types, since the overloads don't follow any substitution rule, you'll have to look at the source code to understand what's happening.
Note: Coding like this is a good exercise, but in real life consider using exceptions as well to not overcomplicate the code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With