About Creating an Alt with Hopac

167 Views Asked by At

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>?

1

There are 1 best solutions below

0
On BEST ANSWER

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!!