12

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
Jared Moore
  • 3,765
  • 26
  • 31

2 Answers2

12

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"
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
2

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"
Dax Fohl
  • 10,654
  • 6
  • 46
  • 90