I want to understand the F# overload resolution a little better in a specific context.
I'm writing a simple asyncResult
workflow/computation expression to make error handling in the style of railway-oriented programming easier to use when combined with async workflows. I do this by overloading the Bind
method on the workflow builder. This is fairly standard and used in all guides I've seen (and is also used in e.g. Chessie/ErrorHandling.fs).
I have one overload that accepts an Async<_>
and one that accepts a Result<_,_>
. Now, ideally, I'd like a third overload that accepts Async<Result<_,_>>
. However, when I try to use let!
or do!
with an expression that returns Async<'a>
, F# complains that a unique overload could not be determined, because both Async<_>
and Async<Result<_,_>>
fits, which of course they do (though one fits more specifically than the other). The only way I seem to be able to do this, is to do like Chessie (link above) and define a wrapper type:
type AsyncResult<'a, 'b> = AR of Async<Result<'a, 'b>>
This again requires me to wrap all calls to methods that return Async<Result<_,_>>
in this new type:
asyncResult {
let! foo = funcReturningResultInsideAsync() |> AR
...
}
AFAIK, C# will select the most specific overload. If F# did the same, this would not be a problem.
Edit: As requested in the comments, here's non-compiling code that shows what I ideally would like, but doesn't work.
module AsyncResult =
let liftAsync x =
async { return x }
let pure (value: 'a) : Async<Result<'a, 'b>> =
async { return Ok value }
let returnFrom (value: Async<Result<'a, 'b>>) : Async<Result<'a, 'b>> =
value
let bind (binder: 'a -> Async<Result<'b, 'c>>) (asyncResult: Async<Result<'a, 'c>>) : Async<Result<'b, 'c>> =
async {
let! result = asyncResult
match result with
| Ok x -> return! binder x
| Error x -> return! Error x |> liftAsync
}
let bindResult (binder: 'a -> Async<Result<'b, 'c>>) (result: Result<'a, 'c>) : Async<Result<'b, 'c>> =
bind binder (liftAsync result)
let bindAsync (binder: 'a -> Async<Result<'b, 'c>>) (asnc: Async<'a>) : Async<Result<'b, 'c>> =
bind binder (Async.map Ok asnc)
type AsyncResultBuilder() =
member __.Return value = pure value
member __.ReturnFrom value = returnFrom value
member __.Bind (asyncResult, binder) = bind binder asyncResult
member __.Bind (result, binder) = bindResult binder result
member __.Bind (async, binder) = bindAsync binder async
let asyncResult = AsyncResultBuilder()
// Usage
let functionReturningAsync () =
async { return 2 }
let errorHandlingFunction () =
asyncResult {
// Error: A unique overload for method 'Bind' could not be determined ...
do! functionReturningAsync()
}
F# overload resolution is very buggy, it has some rules in the spec but in practice it doesn't respect them. I'm tired of reporting bugs about it and seeing how they're being closed in many cases with a (nonsense) 'by-design' resolution.
You can use some tricks to make an overload preferable over the other. One common trick for Builders is to define it as an extension member, so it will have less priority:
module AsyncResult =
let AsyncMap f x = async.Bind(x, async.Return << f)
let liftAsync x =
async { return x }
let pure (value: 'a) : Async<Result<'a, 'b>> =
async { return Ok value }
let returnFrom (value: Async<Result<'a, 'b>>) : Async<Result<'a, 'b>> =
value
let bind (binder: 'a -> Async<Result<'b, 'c>>) (asyncResult: Async<Result<'a, 'c>>) : Async<Result<'b, 'c>> =
async {
let! result = asyncResult
match result with
| Ok x -> return! binder x
| Error x -> return! Error x |> liftAsync
}
let bindResult (binder: 'a -> Async<Result<'b, 'c>>) (result: Result<'a, 'c>) : Async<Result<'b, 'c>> =
bind binder (liftAsync result)
let bindAsync (binder: 'a -> Async<Result<'b, 'c>>) (asnc: Async<'a>) : Async<Result<'b, 'c>> =
bind binder (AsyncMap Ok asnc)
type AsyncResultBuilder() =
member __.Return value = pure value
member __.ReturnFrom value = returnFrom value
member __.Bind (result, binder) = bindResult binder result
member __.Bind (asyncResult, binder) = bind binder asyncResult
let asyncResult = AsyncResultBuilder()
open AsyncResult
type AsyncResultBuilder with
member __.Bind (async, binder) = bindAsync binder async
// Usage
let functionReturningAsync () =
async { return 2 }
let functionReturningAsynResult () =
async { return Ok 'a' }
let errorHandlingFunction () =
asyncResult {
let! x = functionReturningAsync()
let! y = functionReturningAsynResult()
let! z = Ok "worked"
return x, y, z
}
Having said that, I agree 100% with @fyodor-soikin in that it's not a good idea to do this kind of magic for the reasons he explained.
But looks like not everybody agrees with this, apart from Chessie if you have a look at AsyncSeq for example it does some of this magic.
I was criticized over years for abusing of overloading though I was doing it in a consistent way following strict and common accepted rules in general. So I think there are contradictory approaches in the community.
(this ought to be a comment, but it didn't fit)
The general philosophical position of F# is that it's inherently bad to have things "magically" happen behind the scenes. Everything should be written out explicitly, and this is aided by a lighter syntax.
This position is (partly) why F# doesn't have automatic sub/supertype coercions, and this is also why F# is so picky about overload resolution. If F# accepted multiple equally valid overloads, then you wouldn't be able to tell what's going on just by looking at the code. And this is, in fact, exactly what happens in C#: for one example, I can't even remember anymore how many times I had to fix bugs related to IQueryable
/IEnumerable
extension method confusion leading to pulling the whole database from the database server.
I can't say definitively that there isn't some trick to achieve what you're after, but I strongly advise against it.
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