我想在特定的上下文中更好地理解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#做同样的事情,这不会有问题。
修改:根据评论中的要求,此处的非编译代码显示了我理想的内容,但无效。
asyncResult {
let! foo = funcReturningResultInsideAsync() |> AR
...
}
答案 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,例如它会做一些魔术。
多年来,我因滥用超载而受到批评,尽管我一直遵循严格和普遍接受的规则,以一致的方式这样做。所以我认为社区中存在矛盾的方法。