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
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"
There is one issue in your code which is that you're doing a recursive call usingdo! 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"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With