【问题标题】:Why isn't F# able to resolve overload between Async<> and Async<Result<>>?为什么 F# 不能解决 Async<> 和 Async<Result<>> 之间的重载问题?
【发布时间】:2017-12-07 14:16:00
【问题描述】:

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

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

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

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

这再次要求我将所有对返回 Async&lt;Result&lt;_,_&gt;&gt; 的方法的调用封装在这种新类型中:

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

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

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

编辑:根据 cmets 的要求,这里是非编译代码,显示了我理想中想要的,但不起作用。

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()
    }

【问题讨论】:

  • 即使没有编译,您能否发布带有用例的代码?
  • 完成,并修复了问题中的一个错误 - 它是 Async&lt;_&gt;-返回不起作用的函数,而不是 Async&lt;Result&lt;_,_&gt;

标签: f# overload-resolution


【解决方案1】:

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
    }

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

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

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

【讨论】:

  • 这是一个巧妙的技巧,谢谢。一般来说,我同意显式优于隐式,但在这种特殊情况下,我倾向于让实现自动执行“你期望的”(我认为这是一个例子,其中肯定有一个明确和明确的开发人员期望可以满足)。
  • 另外,你指的是“一些技巧”——还有其他的吗?你能举几个你提到的错误报告的例子吗?我很好奇这些错误/功能是什么。
  • 是的,有很多技巧。在F#+ 这样的项目中,我不得不在某种程度上使用它们。关于报告,请参见例如 thisthis
  • 不要吹毛求疵,但这些都不是“按设计”关闭的。第一个将通过 RFC 流程,第二个仍处于活动状态。
  • 两者都被标记为分辨率设计。第二个根据@forki 的要求删除了该标签。第一个显然与 F# 规范相矛盾。在这一点上,我花了很多精力来解决这些限制,我已经习惯了很多技巧。
【解决方案2】:

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

F# 的一般哲学立场是,让事情“神奇”地在幕后发生本质上是不好的。一切都应该明确地写出来,并辅以更简洁的语法。

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

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

【讨论】:

  • 当我读到“让事情‘神奇地’发生在幕后本来就不好”时,我笑了。整个“类型推断”对我来说就像魔术一样。
  • 类型推断遵循严格的规则并且是自洽的。
  • 类型推断不是魔术,只是简单的统一规则,就像使用 Prolog 一样。所谓的 Hindley Milner 类型推断。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-03
  • 2020-03-25
  • 2019-03-29
  • 2015-01-06
  • 1970-01-01
  • 1970-01-01
  • 2021-11-13
相关资源
最近更新 更多