Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'while' in async computation expression where the condition is async

I'm playing around with using SqlClient in F# and I'm having difficulty with using SqlDataReader.ReadAsync. I'm trying to do the F# equivalent of

while (await reader.ReadAsync) { ... }

What is the best way to do this in F#? Below is my full program. It works, but I'd like to know if there is a better way to do it.

open System
open System.Data.SqlClient
open System.Threading.Tasks

let connectionString = "Server=.;Integrated Security=SSPI"

module Async =
    let AwaitVoidTask : (Task -> Async<unit>) =
        Async.AwaitIAsyncResult >> Async.Ignore

    // QUESTION: Is this idiomatic F#? Is there a more generally-used way of doing this?
    let rec While (predicateFn : unit -> Async<bool>) (action : unit -> unit) : Async<unit> = 
        async {
            let! b = predicateFn()
            match b with
                | true -> action(); do! While predicateFn action
                | false -> ()
        }

[<EntryPoint>]
let main argv = 
    let work = async {
        // Open connection
        use conn = new SqlConnection(connectionString)
        do! conn.OpenAsync() |> Async.AwaitVoidTask

        // Execute command
        use cmd = conn.CreateCommand()
        cmd.CommandText <- "select name from sys.databases"
        let! reader = cmd.ExecuteReaderAsync() |> Async.AwaitTask

        // Consume reader

        // I want a convenient 'while' loop like this...
        //while reader.ReadAsync() |> Async.AwaitTask do // Error: This expression was expected to have type bool but here has type Async<bool>
        //    reader.GetValue 0 |> string |> printfn "%s"
        // Instead I used the 'Async.While' method that I defined above.

        let ConsumeReader = Async.While (fun () -> reader.ReadAsync() |> Async.AwaitTask)
        do! ConsumeReader (fun () -> reader.GetValue 0 |> string |> printfn "%s")
    }
    work |> Async.RunSynchronously
    0 // return an integer exit code
like image 458
Jared Moore Avatar asked Jul 26 '15 21:07

Jared Moore


2 Answers

I'd probably do the same as you. If you can stomach refs though, you can shorten it to

let go = ref true
while !go do
  let! more = reader.ReadAsync() |> Async.AwaitTask
  go := more
  reader.GetValue 0 |> string |> printfn "%s"
like image 114
Dax Fohl Avatar answered Nov 16 '22 10:11

Dax Fohl


There is one issue in your code which is that you're doing a recursive call using
do! While predicateFn action. This is a problem because it does not turn into a tail-call and so you could end up with memory leaks. The right way to do this is to use return! instead of do!.

Aside from that, your code works good. But you can actually extend the async computation builder to let you use ordinary while keyword. To do that, you need a slightly different version of While:

let rec While (predicateFn : unit -> Async<bool>) (action : Async<unit>) : Async<unit> = 
    async {
        let! b = predicateFn()
        if b then
            do! action
            return! While predicateFn action
    }

type AsyncBuilder with
    member x.While(cond, body) = Async.While cond body

Here, the body is also asynchronous and it is not a function. Then we add a While method to the computation builder (so we are adding another overload as an extension method). With this, you can actually write:

 while Async.AwaitTask(reader.ReadAsync()) do // This is async!
     do! Async.Sleep(1000)   // The body is asynchronous too
     reader.GetValue 0 |> string |> printfn "%s"
like image 35
Tomas Petricek Avatar answered Nov 16 '22 10:11

Tomas Petricek