«пока» в выражении асинхронного вычисления, где условие является асинхронным

Я играю с использованием SqlClient в F #, и у меня возникают трудности с использованием SqlDataReader.ReadAsync. Я пытаюсь сделать эквивалент F # для

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

Как лучше всего это сделать на F#? Ниже моя полная программа. Это работает, но я хотел бы знать, есть ли лучший способ сделать это.

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

person Jared Moore    schedule 26.07.2015    source источник


Ответы (2)


В вашем коде есть одна проблема: вы выполняете рекурсивный вызов, используя
do! While predicateFn action. Это проблема, потому что это не превращается в хвостовой вызов, и поэтому вы можете столкнуться с утечками памяти. Правильный способ сделать это — использовать return! вместо do!.

Кроме того, ваш код работает хорошо. Но на самом деле вы можете расширить построитель вычислений async, чтобы вы могли использовать обычное ключевое слово while. Для этого вам понадобится немного другая версия 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

Здесь тело также асинхронно и не является функцией. Затем мы добавляем метод While в построитель вычислений (таким образом, мы добавляем еще одну перегрузку в качестве метода расширения). При этом вы можете написать:

 while Async.AwaitTask(reader.ReadAsync()) do // This is async!
     do! Async.Sleep(1000)   // The body is asynchronous too
     reader.GetValue 0 |> string |> printfn "%s"
person Tomas Petricek    schedule 26.07.2015

Я бы, наверное, поступил так же, как ты. Если вы можете переваривать ссылки, вы можете сократить их до

let go = ref true
while !go do
  let! more = reader.ReadAsync() |> Async.AwaitTask
  go := more
  reader.GetValue 0 |> string |> printfn "%s"
person Dax Fohl    schedule 26.07.2015