如何在 F# 异步表达式中使用 while 循环?

3

我该如何用F#编写以下代码?

var reader = await someDatabaseCommand.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
  var foo = reader.GetDouble(1);
  // ... 
}

我对F#非常陌生,这是我能想到的最好的翻译。

task { 
  let! reader = someDatabaseCommand.ExecuteReaderAsync ()
  let! tmp1 = reader.ReadAsync ()
  let mutable hasNext = tmp1
  while hasNext do
    // ...
    let! tmp2 = reader.ReadAsync ()
    hasNext <- tmp2
}

我知道这是一个C#库,所以它不会完美,但我想避免需要将ReadAsync的结果存储在变量中,更不用说有两个临时变量了。我不知道如何在不绑定名称的情况下获取Task的“结果”。也许递归解决方案会起作用?

1个回答

6

这个库很适合使用C#,因为你可以在循环的条件中使用await。我认为在F#中使用递归计算表达式比命令式解决方案更好一些(我认为你不能简化它),但长度大约相同:

task {
  let! reader = someDatabaseCommand.ExecuteReaderAsync ()
  let rec loop () = task { 
    let! hasNext = reader.ReadAsync ()
    if hasNext then
      // ..
      return! loop () }
  return! loop ()
}

更加详细的解决方案是使用F#异步序列(来自于FSharp.Control.AsyncSeq 库)。我认为这只适用于 async (所以你必须使用它而不是 task),但可以使代码更好。你可以定义一个运行命令并返回结果的异步序列的函数(为简单起见,我只返回 reader,这有点不太干净,但也能正常工作):

#r "nuget: FSharp.Control.AsyncSeq"
#r "nuget: System.Data.SqlClient"
open FSharp.Control

let runCommand (cmd:System.Data.SqlClient.SqlCommand) = asyncSeq { 
  let! reader = cmd.ExecuteReaderAsync () |> Async.AwaitTask
  let rec loop () = asyncSeq { 
    let! hasNext = reader.ReadAsync () |> Async.AwaitTask
    if hasNext then
      yield reader
      yield! loop () }
  yield! loop () }

因此,您可以使用AsyncSeq库添加的重载版本,在async内部使用普通的for循环非常好地迭代结果:

async { 
  for reader in runCommand someDatabaseCommand do
    let foo = reader.GetDouble(1) 
    () }

2
值得注意的是,该任务不是尾递归,因此在较长的序列上可能会快速崩溃。 - user1981
@user1981 感谢你指出这个问题!在这种情况下,切换到async可能是有意义的,这样就不会有这个问题了。 - Tomas Petricek
你也可以使用 F# 的 SeqT https://fsprojects.github.io/FSharpPlus//type-seqt.html,这样就不必在 AsyncSeq 之间来回转换了。然后只需在上面的代码中将 Async.AwaitTask 替换为 SeqT.lift,将 asyncSeq 替换为 monad.plus 即可。 - Gus
@user1981 如果我使用Async.AwaitTask将任务转换为异步,那么它就变成尾递归了吗? - Adam
1
@Adam - 是的,我相信是这样的。 - Tomas Petricek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接