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>
?
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 IVar
s 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!!