如何让 Promise 变得懒惰?

How to make the Promise lazy?

open System
open System.Threading
open Hopac
open Hopac.Infixes

let hello what = job {
  for i=1 to 3 do
    do! timeOut (TimeSpan.FromSeconds 1.0)
    do printfn "%s" what
}

run <| job {
  let! j1 = Promise.start (hello "Hello, from a job!")
  do! timeOut (TimeSpan.FromSeconds 0.5)
  let! j2 = Promise.start (hello "Hello, from another job!")
  //do! Promise.read j1
  //do! Promise.read j2
  return ()
}

Console.ReadKey()
Hello, from a job!
Hello, from another job!
Hello, from a job!
Hello, from another job!
Hello, from a job!
Hello, from another job!

这是示例之一 from the Hopac documentation。从我在这里看到的情况来看,即使我没有明确调用 Promise.read j1Promise.read j2,函数仍然会得到 运行。我想知道是否有可能推迟进行承诺的计算,直到它们实际上是 运行?还是我应该使用 lazy 来传播惰性值?

看文档,Hopac 的 promises 似乎是懒惰的,但我不确定这种懒惰应该如何表现。

为了证明懒惰,请考虑以下示例。

module HopacArith

open System
open Hopac

type S = S of int * Promise<S>

let rec arith i : Promise<S> = memo <| Job.delay(fun () ->
    printfn "Hello"
    S(i,arith (i+1)) |> Job.result
    )

let rec loop k = job {
    let! (S(i,_)) = k
    let! (S(i,k)) = k
    printfn "%i" i
    Console.ReadKey()
    return! loop k
    }

loop (arith 0) |> run
Hello
0
Hello
1
Hello
2

如果这些值没有被记忆,每次按下回车键,每次迭代都会打印两个 Hellos。如果删除了 memo <|,就会看到这种行为。

还有一些值得进一步说明的要点。 Promise.start 的目的不是专门为某些工作获取记忆行为。 Promise.startJob.start 的相似之处在于,如果您使用 let!>>= 绑定一个值,它在工作完成之前不会阻塞工作流。然而,与 Job.start 相比,Promise.start 确实提供了一个选项,可以通过绑定嵌套值来等待计划的作业完成。与 Job.start 不同并且类似于常规 .NET 任务,可以从使用 Promise.start.

开始的并发作业中提取值

最后,这是我在玩 promises 时发现的一个有趣的花絮。事实证明,将 Job 转换为 Alt 的一个好方法是先将其转换为 Promise,然后再向上转换。

module HopacPromiseNonblocking

open System
open Hopac
open Hopac.Infixes

Alt.choose [
    //Alt.always 1 ^=>. Alt.never () // blocks forever
    memo (Alt.always 1 ^=>. Alt.never ()) :> _ Alt // does not block
    Alt.always 1 >>=*. Alt.never () :> _ Alt // same as above, does not block
    Alt.always 2
    ]
|> run
|> printfn "%i" // prints 2

Console.ReadKey()

取消对第一种情况的注释会导致程序永远阻塞,但如果您首先记住表达式,那么如果使用常规替代方案,则可能会出现回溯行为。