Asik
Asik

Reputation: 22133

How to preserve generic type in async computation expression?

Why is the generic type constrained to unit, and how can I write this in a way that myFunc gets properly typed as 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"
    }

Edit: I was hoping this would make for a good minimal repro, but here's what I actually want to do:

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

Upvotes: 0

Views: 49

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

Every expression must have a definite type, including a try ... with expression. Having a definite type, in this case, means that both the try and the with branches must have the same type.

But in your code, the try branch returns 't, while the with branch returns unit. But no matter: since 't can be anything, we can just unify it with unit, and now the whole try ... with expression returns unit as well. Problem solved!

To fix this, you need to have the with branch return a 't as well. How to do that, where to get a 't? I'm afraid I can't help you there, you must decide.

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

But from the overall shape of the code, I suspect that what you really meant was to put the failwith under the with branch as well. Because failwith can have any type you want, this will make the compiler happy with keeping the type of the whole try ... with block as 't:

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

(note that there is now an extra return keyword inside with - that's because without a return the async block is assumed to have type unit, and we're back to the same problem)


Responding to your comments, it looks like what you're actually trying to do is an endless iteration, limited by a timeout, and you want to keep iterating as long as there is an error.

The problem with your code, however, is that it won't actually work the way you expect, even if you somehow return a value of 't from the with branch. This is because the return! keyword doesn't "interrupt execution" like it does in C# (formally known as "early return"), but merely runs the given func() and makes its value the result of the current block.

In fact, if you remove the with branch entirely, the problem will persist: the compiler will still insist on the type of func() being unit. This is because the call is inside a while loop, and the body of the while loop must not return a value, otherwise that value effectively gets thrown away, so there must be a mistake of some sort. But it's ok to throw away a unit, so the compiler allows it.

A good way to do what you're trying to do is via recursion:

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

Here, the function loop first checks the timeout and throws (note also the return keyword - that's to satisfy the type system), otherwise runs func(), but if that fails, waits a bit and calls itself recursively, thus continuing the process.

This general scheme (or stuff built on top of it) is how all iteration is modeled in functional programming. Forget about loops, they're not helpful.

Upvotes: 1

Related Questions