为什么F#不能解决Async <>和Async <Result <>>之间的重载问题?

cme*_*ren 4 f# overload-resolution

我想在特定的上下文中更好地理解F#重载决策.

我正在编写一个简单的asyncResult工作流程/计算表达式,以便在与异步工作流程结合使用时,以面向铁路的编程风格更容易使用错误处理.我通过Bind在工作流构建器上重载方法来完成此操作.这是相当标准的,并且在我看过的所有指南中使用(并且也用于例如Chessie/ErrorHandling.fs).

我有一个接受一个Async<_>和一个接受一个的重载Result<_,_>.现在,理想情况下,我想接受第三个重载Async<Result<_,_>>.然而,当我尝试使用let!do!与返回表达式Async<'a>,F#抱怨独特的超载不能确定,因为这两个Async<_>Async<Result<_,_>>配合,这当然他们这样做(虽然其中一个比另一个更适合明确).我似乎能够做到这一点的唯一方法就是像Chessie(上面的链接)那样做并定义一个包装类型:

type AsyncResult<'a, 'b> = AR of Async<Result<'a, 'b>>
Run Code Online (Sandbox Code Playgroud)

这再次要求我将所有调用包装到Async<Result<_,_>>以这种新类型返回的方法中:

asyncResult {
  let! foo = funcReturningResultInsideAsync() |> AR
  ...
}
Run Code Online (Sandbox Code Playgroud)

AFAIK,C#将选择最具体的过载.如果F#做了同样的事情,这不会有问题.

  1. 为什么F#不能选择最具体的过载?
  2. 在这种特定情况下可以采取任何措施来避免包装类型吗?

编辑:根据评论中的要求,这里是非编译代码,显示我理想的内容,但不起作用.

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()
    }
Run Code Online (Sandbox Code Playgroud)

Fyo*_*kin 5

(这应该是一个评论,但它不合适)

F#的一般哲学立场是,在幕后"神奇地"发生事情本质上是不好的.应该明确地写出所有内容,这可以通过更轻松的语法来帮助.

这个位置(部分)是为什么F#没有自动子/超类型强制,这也是为什么F#对重载决策过于挑剔的原因.如果F#接受多个同等有效的重载,那么仅仅通过查看代码就无法分辨出正在发生的事情.事实上,这正是C#中发生的事情:例如,我甚至不记得有多少次我必须修复与IQueryable/ IEnumerableextension方法混淆相关的错误,导致从数据库服务器中拉出整个数据库.

我不能肯定地说,没有一些技巧可以实现你所追求的目标,但我强烈建议反对它.

  • 类型推断遵循严格的规则并且是自洽的. (4认同)

Gus*_*Gus 5

F#重载解析是非常有问题的,它在规范中有一些规则,但在实践中不遵守它们。我厌倦了报告有关此问题的错误,并看到在许多情况下(无意义的)“按设计”解决方案如何关闭它们。

您可以使用一些技巧来使重载优于其他技巧。Builders的一个常见技巧是将其定义为扩展成员,因此优先级较低:

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
    }
Run Code Online (Sandbox Code Playgroud)

话虽这么说,我100%同意@ fyodor-soikin,因为他解释的原因,做这种魔术不是一个好主意。

但是看起来似乎不是每个人都同意这一点,除了Chessie 之外,例如,如果您看一下AsyncSeq,它确实起到了一些作用。

尽管多年来我一直遵循严格且普遍接受的规则以一致的方式进行滥用,但多年来我一直因滥用过载而受到批评。因此,我认为社区中存在矛盾的方法。