Search code examples
concurrencypromisef#hopac

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!

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.


Solution

  • 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.