Timeouts in F#

Timeouts are important for programs that connect to external resources or that otherwise need to bound execution time. Implementing timeouts in F# is straightforward using asynchronous workflows and the Async.Race function.

open System
open System.Threading.Tasks

let simulateExternalCall delay result =
    async {
        do! Async.Sleep(delay)
        return result
    }

let timeout duration =
    async {
        do! Async.Sleep duration
        return None
    }

let main() =
    // For our example, suppose we're executing an external
    // call that returns its result after 2 seconds.
    let externalCall1 = simulateExternalCall 2000 "result 1"

    // Here we implement a timeout using Async.Race.
    // It will run both the external call and the timeout
    // concurrently, returning the result of whichever
    // finishes first.
    let result1 = 
        Async.RunSynchronously(
            Async.Race(externalCall1, timeout 1000))

    match result1 with
    | Choice1Of2 res -> printfn "%s" res
    | Choice2Of2 _ -> printfn "timeout 1"

    // If we allow a longer timeout of 3s, then the external
    // call will succeed and we'll print the result.
    let externalCall2 = simulateExternalCall 2000 "result 2"
    let result2 = 
        Async.RunSynchronously(
            Async.Race(externalCall2, timeout 3000))

    match result2 with
    | Choice1Of2 res -> printfn "%s" res
    | Choice2Of2 _ -> printfn "timeout 2"

main()

Running this program shows the first operation timing out and the second succeeding.

$ dotnet fsi timeouts.fsx
timeout 1
result 2

In this F# implementation, we use Async.Sleep to simulate delays and external calls. The Async.Race function is used to implement the timeout mechanism, which is similar to the select statement in the original example. It runs two asynchronous workflows concurrently and returns the result of whichever finishes first.

The Choice1Of2 and Choice2Of2 in the pattern matching correspond to the success and timeout cases respectively. This approach provides a clean and idiomatic way to handle timeouts in F#.