为什么 F# 无法解决 Async<> 和 Async<Result<>> 之间的过载问题?

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

我想在特定上下文中更好地理解 F# 重载解析。

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

我有一个接受 Async<_> 的重载和一个接受 Result<_,_> 的重载。现在,理想情况下,我想要接受 Async<Result<_,_>> 的第三个重载。但是,当我尝试将 let!do! 与 returns Async<'a> 的表达式一起使用时,F# 抱怨无法确定唯一重载,因为 [=16] =] 和 Async<Result<_,_>> 适合,它们当然适合(尽管一个比另一个更适合)。我似乎能够做到这一点的唯一方法是像 Chessie(上面的link)那样做并定义一个包装器类型:

type AsyncResult<'a, 'b> = AR of Async<Result<'a, 'b>>

这又要求我将对 return Async<Result<_,_>> 方法的所有调用包装在这个新类型中:

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

AFAIK,C# 将 select 最具体的重载。如果 F# 也这样做,这将不是问题。

  1. 为什么 F# 不能 select 最具体的重载?
  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()
    }

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

F# 的一般哲学立场是让事情 "magically" 在幕后发生本质上是不好的。一切都应该明确地写出来,这得益于更轻便的语法。

这个位置(部分)是 F# 没有自动 sub/supertype 强制转换的原因,这也是 F# 对重载决议如此挑剔的原因。如果 F# 接受多个同样有效的重载,那么您将无法仅通过查看代码来判断发生了什么。事实上,这正是 C# 中发生的事情:例如,我什至不记得有多少次我不得不修复与 IQueryable/IEnumerable 扩展方法混淆相关的错误,导致从数据库服务器中拉取整个数据库。

我不能肯定地说没有什么技巧可以实现您所追求的目标,但我强烈建议您不要这样做。

F# 重载解析非常有问题,它在规范中有一些规则,但在实践中它并不遵守这些规则。我厌倦了报告有关它的错误并看到它们在许多情况下是如何通过(废话)'by-design' 解决方案关闭的。

您可以使用一些技巧使重载优于其他重载。 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
    }

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

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

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