Search code examples
f#hopac

About Creating an Alt with Hopac


When I use Hopac to create Alt<unit> with Alt.<functions> like always or once it would lead me to strange negative acknowledgement outcome.

But if I use async {<expression>} to create Alt<unit> then everything works as expected.

open Hopac
open Hopac.Core
open Hopac.Infixes
open Hopac.Extensions

let pf m (s:int) = Alt.prepareFun <| fun _ ->
    Alt.always () ^=> fun _ ->
        job { 
            printfn "starting [%s] %d" m Thread.CurrentThread.ManagedThreadId
            Thread.Sleep s
            printfn "%s" m }
        |> Job.start

let delayedPrintn3 msg delayInMillis =
  Alt.prepareFun <| fun _ ->     
    async {
        printfn "starting [%s] %d" msg Thread.CurrentThread.ManagedThreadId
        do! Async.Sleep delayInMillis
    }
    |> Alt.fromAsync
    |> Alt.afterFun (fun _ -> printfn "%s" msg)

let na : (string -> int -> Alt<unit>) -> string -> string -> int -> Alt<unit> = fun ff s1 s2 i ->
    Alt.withNackJob <|
        fun nack ->        
            nack
            |> Alt.afterFun (fun () ->
                  printfn "%s" s1)
            |> Job.start 
            |> Job.map (fun _ -> ff s2 i)

let na11 = na delayedPrintn3 "1 canceled!!" "na11" 3
let na22 = na delayedPrintn3 "2 canceled!!" "na22" 0

let na33 = na pf "1 canceled!!" "na33" 3
let na44 = na pf "2 canceled!!" "na44" 0

na22 <|> na11 |> run
na33 <|> na44 |> run

And the result are:

starting [na22] 18
starting [na11] 18
na22
1 canceled!!

and

starting [na33] 11
na33

However I want to get the same result. What's the problem when using Alt.<function>?


Solution

  • Hopac Alt's are extremely tricky and it took me a while to get them right.

    When you are returning Alt to prepareFun/prepareJob, you're going to want to return an Alt that hasn't been committed to. In your sample for pf you're returning Alt.always which means this Alt is committed to always. So when calling na33 <|> na44 |> run this means na33 has already been committed to and doesn't need to run na44.

    In contrast, example of delayedPrintn3 is using Async and if you look at the reference implementation from https://github.com/Hopac/Hopac/blob/master/Docs/Alternatives.md

    open System.Threading
    
    let asyncAsAlt (xA: Async<'x>) : Alt<'x> = Alt.withNackJob <| fun nack ->
      let rI = IVar ()
      let tokenSource = new CancellationTokenSource ()
      let dispose () =
        tokenSource.Dispose ()
        // printfn "Dispose"
      let op = async {
          try
            let! x = xA
            do rI *<= x |> start
            // do printfn "Success"
          with e ->
            do rI *<=! e |> start
            // do printfn "Failure"
        }
      Async.Start (op, cancellationToken = tokenSource.Token)
      nack
      >>- fun () ->
            tokenSource.Cancel ()
            // printfn "Cancel"
            dispose ()
      |> Job.start >>-.
      Alt.tryFinallyFun rI dispose
    

    it's creating an IVar (think of them as the same as TaskCompletionSource) which later will be set after the Async op is started. So in your example you can see both being started since their IVars haven't been committed to yet.

    If you're looking for a similar implementation, something like:

    let pf2 m (s:int) = Alt.prepareJob <| fun _ ->
        let retVal = IVar<unit>()
        job { 
            printfn "starting [%s] %d" m Thread.CurrentThread.ManagedThreadId
            do! timeOutMillis s
            printfn "%s" m 
            do! IVar.fill retVal ()
        }
        |> Job.start
        >>-. retVal
    
    

    Which returns an IVar (which is an Alt) that hasn't been committed to. I had to up the sleep time to 100 to make sure Hopac didn't commit to the first one too quickly.

    
    let na55 = na pf2 "1 canceled!!" "na55" 100
    let na66 = na pf2 "2 canceled!!" "na66" 0
    
    
    starting [na55] 9
    starting [na66] 9
    na66
    1 canceled!!