Search code examples
f#taskmutable

How to execute an array of tasks sequentially?


I have the following code that I want to review

printfn "calling openai embeddings API"
let sw = System.Diagnostics.Stopwatch ()
sw.Start ()
let embs : MyRedis.RedisEmbedding array = 
    docs 
    |> MyOpenAI.rebuild2EmbeddingsSlow             
    
    // here the part of the code to be defined and reviewed
    
sw.Stop ()
printfn "Artificially slowed down to %f" (sw.Elapsed.TotalSeconds)

The goal of the code is to show the timing of the sequential execution of an array of tasks in the log and possibly also to guarantee that the execution is sequential and not parallel, as it would be, I think, with a simpler Task.WhenAll.

Though I 've had to introduce a mutable accumulator because Task.WhenAll returns a Task that is not yet executed and I was not able to insert the timing part within it.

Also, something like

    |> Array.fold (fun s t -> 
        printfn "slow down - intermediate elapsed start %f " (sw.Elapsed.TotalSeconds)
        let! e1 = t
        printfn "intermediate elapsed end %f " (sw.Elapsed.TotalSeconds)
        Array.concat [| e1; s |]
    ) 

would not compile because of the let!

But I want to be sure that the following code is F# idiomatic and that the usage of a mutable keyword is unavoidable.

printfn "calling openai embeddings API"
let sw = System.Diagnostics.Stopwatch ()
sw.Start ()
let embs : MyRedis.RedisEmbedding array = 
    docs 
    |> MyOpenAI.rebuild2EmbeddingsSlow             
    |> Array.fold (fun s t -> 
        let mutable acc = [||]
        printfn "slow down - intermediate elapsed start %f " (sw.Elapsed.TotalSeconds)
        task {
            let! e1 = t
            acc <- Array.append e1 s
        } |> Task.WaitAll
        printfn "intermediate elapsed end %f " (sw.Elapsed.TotalSeconds)
        acc
        ) 
        [| |]
sw.Stop ()
printfn "Artificially slowed down to %f" (sw.Elapsed.TotalSeconds)

I think that the let! binding cannot be used directly inside the Array.fold function as it is outside the computation expression, hence the code does indeed use a mutable variable acc correctly within the Array.fold function and the mutable variable is necessary to accumulate the embeddings in the array.

Is that agreeable? Is the code F# idiomatic?


Solution

  • Here's an example of creating an array of F# asyncs from underlying .NET tasks, and then executing them sequentially.

    To start with, I wrote a dummy function that mimics a call to OpenAI's API:

    let openAiTask i =
        task {
            do! Task.Delay(1000)
            return! Task.FromResult i
        }
    

    Then I created a batch of F# async calls to the API:

    let asyncs =
        let sw = System.Diagnostics.Stopwatch()
        sw.Start()
        [| 1..10 |]
            |> Array.map (fun i ->
                async {
                    printfn "%A" sw.Elapsed.TotalSeconds
                    return! openAiTask i |> Async.AwaitTask
                })
    

    Now I can execute the async calls sequentially whenever I want:

    let results =
        Async.Sequential asyncs
            |> Async.RunSynchronously
    printfn "%A" results
    

    Note that none of the API calls occur until I explicitly run them. Output is something like:

    0.0238377
    1.0503682
    2.0602829
    3.0719534
    4.0832107
    5.0838953
    6.0951127
    7.1090538
    8.1239493
    9.1347231
    [|1; 2; 3; 4; 5; 6; 7; 8; 9; 10|]