How to make the Promise lazy?

62 views Asked by At
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!

This is one of the examples from the Hopac documentation. From what I can see here, even if I do not explicitly call Promise.read j1 or Promise.read j2 the functions still get run. I am wondering if it is possible to defer doing the promised computation until they are actually run? Or should I be using lazy for the purpose of propagating lazy values?

Looking at the documentation, it does seem like Hopac's promises are supposed to be lazy, but I am not sure how this laziness is supposed to be manifested.

1

There are 1 answers

0
Marko Grdinić On BEST ANSWER

For a demonstration of laziness, consider the following example.

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

Had the values not been memoized, every time the enter is pressed, there would be two Hellos printed per iteration. This behavior can be seen if memo <| is removed.

There are some further points worth making. The purpose of Promise.start is not specifically to get memoizing behavior for some job. Promise.start is similar to Job.start in that if you bind a value using let! or >>= for example, it won't block the workflow until work is done. However compared to Job.start, Promise.start does give an option to wait for the scheduled job to be finished by binding on the nested value. And unlike Job.start and similarly to regular .NET tasks, it is possible to extract the value from a concurrent job started using Promise.start.

Lastly, here is an interesting tidbit I've discovered while playing with promises. It turns out, a good way of turning a Job into an Alt is to turn it into an Promise first and then upcast it.

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()

Uncommenting that first case would cause the program to block forever, but if you memoize the expression first that it is possible to get what would be backtracking behavior had the regular alternatives been used.