【问题标题】:How to preserve generic type in async computation expression?如何在异步计算表达式中保留泛型类型?
【发布时间】:2020-07-10 14:54:26
【问题描述】:

为什么泛型类型被限制为单位,我怎样才能以 myFunc 被正确键入为 unit -> Async<'t> 的方式编写它?

let myFunc (func: unit -> Async<'t>) = // myFunc: unit -> Async<unit>
    async {
        try
            do! Async.Sleep 500
            return! func() // 't constrained to unit here
        with _ex ->
            do! Async.Sleep 200
        
        failwith "failed"
    }

编辑:我希望这将是一个很好的最小复制,但这是我真正想做的:

let retryUntilTimeout (func: unit -> Async<'t>) timeout =
    async {
        let sw = Stopwatch.StartNew()
        while sw.ElapsedMilliseconds < timeout do
            try
                return! func ()
            with _ex ->
                printfn "failed"
                do! Async.Sleep 200

        raise (TimeoutException())
    }

【问题讨论】:

    标签: f#


    【解决方案1】:

    每个表达式都必须有明确的类型,包括try ... with 表达式。在这种情况下,具有明确的类型意味着trywith 分支必须具有相同的类型。

    但在您的代码中,try 分支返回't,而with 分支返回unit。但没关系:既然't 可以是任何东西,我们可以将它与unit 统一起来,现在整个try ... with 表达式也返回unit。问题解决了!

    要解决此问题,您还需要让 with 分支返回 't。怎么做,在哪里可以得到't?恐怕我帮不了你,你必须决定。

    let myFunc (func: unit -> Async<'t>) = 
        async {
            try
                do! Async.Sleep 500
                return! func()
            with _ex ->
                do! Async.Sleep 200
                return someOtherValueOfT
            
            failwith "failed"
        }
    

    但从代码的整体形状来看,我怀疑您的真正意思是将failwith 也放在with 分支下。因为failwith 可以有你想要的任何类型,这将使​​编译器很高兴将整个try ... with 块的类型保持为't

    let myFunc (func: unit -> Async<'t>) =
        async {
            try
                do! Async.Sleep 500
                return! func()
            with _ex ->
                do! Async.Sleep 200   
                return failwith "failed"
        }
    

    (请注意,现在with 中多了一个return 关键字——这是因为没有return,异步块被假定为unit 类型,我们又回到了同样的问题)


    响应您的 cmets,看起来您实际上尝试做的是无限迭代,受超时限制,并且只要有错误,您就希望继续迭代。

    但是,您的代码的问题在于,即使您以某种方式从with 分支返回了't 的值,它实际上也不会按您期望的方式工作。这是因为 return! 关键字不像在 C# 中那样“中断执行”(正式称为“提前返回”),而只是运行给定的 func() 并将其值作为当前块的结果。

    事实上,如果你完全删除with 分支,问题仍然存在:编译器仍会坚持func() 的类型为unit。这是因为调用在while 循环内,while 循环的主体不能返回值,否则该值实际上会被丢弃,所以一定有某种错误。不过把unit扔掉也没关系,所以编译器允许了。

    做你想做的事的一个好方法是通过递归:

    let retryUntilTimeout (func: unit -> Async<'t>) timeout =
        let sw = Stopwatch.StartNew()
    
        let rec loop () = async { 
            if sw.ElapsedMilliseconds > timeout 
            then 
                return raise (TimeoutException())
            else
                try
                    return! func ()
                with _ex ->
                    printfn "failed"
                    do! Async.Sleep 200
                    return! loop ()
        }
    
        loop ()
    

    这里,函数loop首先检查超时并抛出(还要注意return关键字-这是为了满足类型系统),否则运行func(),但如果失败,稍等片刻并调用自己递归,从而继续该过程。

    这个通用方案(或建立在它之上的东西)是函数式编程中所有迭代的建模方式。忘记循环,它们没有帮助。

    【讨论】:

    • 感谢详细回答。恐怕两者都不适用于我的用例,我没有在 OP 中描述。我已经添加了我实际上正在尝试编写的函数;如果您也可以就如何编写它提出建议,我将不胜感激。
    • 更新了答案
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    相关资源
    最近更新 更多