Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

f# perform await async methods in list.iteri

I have the following code in F# 4.0

let processEscalation escalationAction (escalationEvents:UpdateCmd.Record list) =
    printf "%A" Environment.NewLine
    printf "Started %A" escalationAction
    escalationEvents
    |> List.iter ( fun x -> 
        printf "%A" Environment.NewLine
        printf "escalation %A for with action: %A" x.incident_id escalationAction
        service.PostAction(new Models.Action(x.incident_id, escalationAction, "escalated")) 
        |> Async.AwaitTask
        |> ignore)


let ComposeEscalation() = 
    let escalationlevels = ["ESC1 REACHED"; "ESC2 REACHED"; "ESC3 REACHED"]
    escalationlevels 
    |> List.map getEscalationEvents
    |> List.iteri (fun i x -> processEscalation escalationlevels.[i] x)

where the following line is a call to a C# async method that that returns Task

service.PostAction(new Models.Action(x.incident_id, escalationAction, "escalated"))

The compose escalation method calls the processEscalation three times. However, the second call starts before the first call is complete. How can I make sure that the the last line, list.iteri awaits and processes them sequentially? Perhaps the processEscalation should be in an async computation expression?

like image 331
Chinwobble Avatar asked Dec 30 '16 11:12

Chinwobble


1 Answers

What Async.AwaitTask does is that it returns an Async computation that can be used to wait for the task to complete. In your case, you never do anything with it, so the loop just proceeds to the next iteration.

You want something like this:

service.PostAction(new Models.Action(x.incident_id, escalationAction, "escalated")) 
|> Async.AwaitTask
|> Async.RunSynchronously
|> ignore

This should have the effect you expect, though certainly there are nicer, more composable ways of expressing such logic.

Edit: What I meant was something like this, a counterpart to the core Async.Parallel function:

module Async = 

    let sequential (asyncs: seq<Async<'t>>) : Async<'t []> = 
        let rec loop acc remaining = 
            async {
                match remaining with
                | [] -> return Array.ofList (List.rev acc)
                | x::xs ->
                    let! res = x
                    return! loop (res::acc) xs
            }
        loop [] (List.ofSeq asyncs)

Then you can do something along these lines:

escalationEvents
// a collection of asyncs - note that the task won't start until the async is ran
|> List.map (fun x -> 
    async {
        let task = 
            service.PostAction(new Models.Action(x.incident_id, escalationAction, "escalated")) 
        return! Async.AwaitTask task 
    })
// go from a collection of asyncs into an async of a collection
|> Async.sequential
// you don't care about the result, so ignore it
|> Async.Ignore
// now that you have your async, you need to run it in a way that makes sense
// in your context - Async.Start could be another option. 
|> Async.RunSynchronously

The upside here is that instead of bundling everything into a single loop, you've split the computation into well-delimited stages. It's easy to follow and refactor (e.g. if you need to process those events in parallel instead, you just switch out one step in the pipeline).

like image 95
scrwtp Avatar answered Oct 27 '22 11:10

scrwtp