为什么F#无法解决Async&lt;&gt;之间的重载问题和Async <result <>&gt;?

时间:2017-12-07 14:16:01

标签: f# overload-resolution

我想在特定的上下文中更好地理解F#重载分辨率。

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

我有一个接受Async<_>的重载和一个接受Result<_,_>的重载。现在,理想情况下,我喜欢接受Async<Result<_,_>>的第三个重载。但是,当我尝试将let!do!与返回Async<'a>的表达式一起使用时,F#会抱怨无法确定唯一的重载,因为Async<_>和{{{ 1}}适合,当然他们这样做(虽然一个比另一个更具体)。我似乎能够做到这一点的唯一方法就是像Chessie(上面的链接)那样定义一个包装类型:

Async<Result<_,_>>

这再次要求我将所有调用包装到这个新类型中返回type AsyncResult<'a, 'b> = AR of Async<Result<'a, 'b>> 的方法:

Async<Result<_,_>>

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

  1. 为什么F#选择最具体的过载?
  2. 在这种特定情况下,可以采取任何措施来避免包装类型吗?
  3. 修改:根据评论中的要求,此处的非编译代码显示了我理想的内容,但无效。

    asyncResult {
      let! foo = funcReturningResultInsideAsync() |> AR
      ...
    }
    

2 个答案:

答案 0 :(得分:5)

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

F#的一般哲学立场是,拥有东西本身就很糟糕,并且神奇地#34;在幕后发生。一切都应该明确写出来,这可以通过更轻松的语法来帮助。

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

我无法明确表示没有一些技巧可以实现你所追求的目标,但我强烈建议反对它。

答案 1 :(得分:5)

F#重载决议是非常错误的,它在规范中有一些规则,但在实践中它不尊重它们。我已经厌倦了报告它的错误,并且在很多情况下看到它们是如何通过(废话)“按设计”解决方案被关闭的。

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

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
    }

话虽如此,我同意@fyodor-soikin的100%认为,出于他解释的原因,做这种魔术并不是一个好主意。

但看起来不是每个人都同意这一点,除了Chessie,如果你看一下AsyncSeq,例如它会做一些魔术。

多年来,我因滥用超载而受到批评,尽管我一直遵循严格和普遍接受的规则,以一致的方式这样做。所以我认为社区中存在矛盾的方法。