Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't F# able to resolve overload between Async<> and Async<Result<>>?

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.

  1. Why can't F# select the most specific overload?
  2. Can anything be done to avoid the wrapper type in this specific situation?

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()
    }
like image 746
cmeeren Avatar asked Dec 14 '22 19:12

cmeeren


2 Answers

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.

like image 126
Gus Avatar answered Dec 17 '22 23:12

Gus


(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.

like image 45
Fyodor Soikin Avatar answered Dec 18 '22 00:12

Fyodor Soikin