Tamizh in words

Concurrent Programming in Fsharp Using Hopac - Part 6

Published on
Read Time
ยท 8 min read

Hi there!

Welcome back to the sixth part of my blog series on concurrent programming in fsharp. In this part, we are going to learn how to deal with state changes while doing concurrent programming through a fun example.

Time Bomb Simulator

The example that we are going to see is a time bomb simulator. The time bomb transitions through different states as shown below during its lifecycle.

The associated fsharp type TimeBomb will have the following signature

type TimeBomb =
class
new : unit -> TimeBomb
member Activate : seconds:int * defuseChar:char -> unit
member Status : unit -> Status
member TryDefuse : defuseChar:char -> unit
member DeadStatusAlt : Hopac.Alt<Reason>
member SecondsRemainingCh : Hopac.Ch<int>
end

The time bomb will be initially in NotActivated state and moves to Alive state on a method call Activate. In this method call, we are going to specify the seconds that the time bomb has to wait before triggering the detonation. To support defuse, we are also going to define a unique character which defuses an alive time bomb.

In Alive state, the time bomb sends the seconds remaining to the outside world via a Hopac Channel SecondsRemainingCh. While it is alive, we can call the TryDefuse with any character to defuse it.

If the specified character in the TryDefuse method matches with the character that we provided during activation, the time bomb will go the Dead state with the value Defused.

If none of the attempt succeeds in the stipulated time, the time bomb will go the Dead state with the value Exploded.

The dead status change is communicated through DeadStatusAlt.

The Implementation

Let's start with defining the types to represent the time bomb's status

type Reason =
| Exploded
| Defused

type Status =
| NotActivated
| Alive
| Dead of Reason

The TimeBomb type is going to have two internal states reason, to capture the Reason for the Dead status and activated, to store whether the time bomb is activated or not.

type TimeBomb () =
// IVar<Reason>
let reason = IVar<Reason>()
// IVar<unit>
let activated = IVar()

We are making use of the Hopac's write once variable abstraction IVar to define the internal states as we did in the [last blog post](/blog/concurrent-programming-in-fsharp-using-hopac-part-5.mmark" >}}) to model the Ticker state.

The next step is exposing the status of the time bomb.

type TimeBomb () =
// ...

// Status
member __.Status
with get() =
let deadReasonAlt = // // <1>
IVar.read reason
|> Alt.afterFun Dead

let activatedAlt = // // <2>
IVar.read activated
|> Alt.afterFun (fun _ -> Alive)

let notActivatedAlt = // <3>
Alt.always NotActivated

Alt.choose [
deadReasonAlt
activatedAlt
notActivatedAlt]
|> run // <4>

<1> The deadReasonAlt will be available when the reason IVar is populated.

<2> The activatedAlt will be available when activated IVar is populated.

<3> The notActivatedAlt is the default state, that'll always be available. (Like a default case in a switch statement)

val always: 'x -> Alt<'x>

Creates an alternative that is always available and results in the given value.

<4> We are choosing between the above three Alts.

Then we are going to leverage the Ticker component we created in the [last blog post](/blog/concurrent-programming-in-fsharp-using-hopac-part-5.mmark" >}}) to send the seconds remaining via SecondsRemainingCh.

type TimeBomb () =
// ...
let secondsRemainingCh = Ch<int>()

// Ticker -> int -> Alt<'a>
let rec onTick (ticker : Ticker) secondsRemaining =
ticker.C
|> Alt.afterJob (fun _ -> Ch.send secondsRemainingCh secondsRemaining)
|> Alt.afterJob (fun _ -> onTick ticker (secondsRemaining - 1))

// int -> Ticker
let startTicker seconds =
let ticker = new Ticker(TimeSpan.FromSeconds 1.)
onTick ticker (seconds - 1) |> start
ticker

// ...

member __.SecondsRemainingCh
with get() = secondsRemainingCh

To model the explosion of the time bomb, let's define a startTimeOut function which takes the time bomb's actual seconds remaining during activation and uses timeOut function from Hopac to modify the internal state reason after the given delay.

type TimeBomb () =
// ...

// int -> unit
let startTimeOut seconds =
let timeOutAlt =
seconds
|> float
|> TimeSpan.FromSeconds
|> timeOut

timeOutAlt
|> Alt.afterJob (fun _ ->
IVar.tryFill reason Exploded)
|> start

Now, we have all the required functions to expose the Activate method. So, let's put it together.

type TimeBomb () =
// ...

// int -> Char -> unit
let activate seconds =
let ticker = startTicker seconds // <1>
startTimeOut seconds // <2>
IVar.tryFill activated () |> start // <3>
IVar.read reason
|> Alt.afterFun (fun _ -> ticker.Stop()) // <4>
|> start
// ...

// int -> unit
member this.Activate (seconds : int) =
match this.Status with
| NotActivated -> activate seconds // <5>
| _ -> ()

member __.DeadStatusAlt
with get() = IVar.read reason // <6>

<1> Starts the ticker

<2> Starts the timer to keep track of the time to detonate the time bomb

<3> Fills the activated IVar to update the Status.

<4> Stops the ticker, when the time bomb is dead

<5> Activates the time bomb only if it's in NotActivated status.

<6> Exposes an Alt to communicate that the time bomb is dead.

Now it's time to simulate the time bomb explosion.

// TimeBomb -> unit
let printSecondsRemaining (t : TimeBomb) =
t.SecondsRemainingCh
|> Alt.afterFun (printfn "Seconds Remaining: %d")
|> Job.foreverServer |> start

// unit -> unit
let simulateExplosion () =
let seconds = 5
let t = TimeBomb()
t.Status |> printfn "Status: %A"
t.Activate(seconds)
printSecondsRemaining t
t.Status |> printfn "Status: %A"
t.DeadStatusAlt
|> Alt.afterFun (printfn "Status: %A")
|> run

The simulateExplosion function creates a TimeBomb with five seconds as detonation time and prints the statuses & the seconds remaining.

> simulateExplosion ();;
Status: NotActivated
Status: Alive
Seconds Remaining: 4
Seconds Remaining: 3
Seconds Remaining: 2
Seconds Remaining: 1
Seconds Remaining: 0
Status: Exploded
Real: 00:00:05.054, CPU: 00:00:00.078, GC gen0: 0, gen1: 0
val it : unit = ()

Awesome!

Adding Support For Defuse

Like we see in movies, a time bomb has to have a provision to defuse! Adding this to our TimeBomb implementation is straightforward.

Unlike the real time bomb, instead of providing some random coloured wire to defuse the bomb, we are going to emulate this via random char.

type TimeBomb () =
// ...

// Ch<char>
let inCh = Ch<char>() // <1>

// char -> Alt<unit>
let rec inputLoop defuseChar =
let onInput inChar =
if inChar = defuseChar then
IVar.tryFill reason Defused
else
inputLoop defuseChar :> Job<unit>
inCh
|> Alt.afterJob onInput // <2>

// ...

<1> Adds a new internal state inCh.

<2> On every input on the inCh, we are matching this input with the defuseChar. If it matches, we transition the status of the TimeBomb to Dead with the reason Defused else we continue the loop.

type TimeBomb () =
// ...

// char -> unit
member this.TryDefuse(defuseChar) =
match this.Status with
| Alive ->
Ch.give inCh defuseChar
|> start
| _ -> ()

Using the TryDefuse method, the consumer of TimeBomb can input the defuseChar, and it will be put into the inCh only if the TimeBomb is in Alive status.

The final step is modifying the activate function to support defuse.

type TimeBomb () =
// ...

- let activate seconds =
+ let activate seconds defuseChar =
let ticker = startTicker seconds
startTimeOut seconds
IVar.tryFill activated () |> start
+ inputLoop defuseChar |> start
IVar.read reason
|> Alt.afterFun (fun _ -> ticker.Stop())
|> start

// ...
- member this.Activate (seconds : int) =
+ member this.Activate (seconds : int, defuseChar : char) =
match this.Status with
| NotActivated ->
- activate seconds
+ activate seconds defuseChar
| _ -> ()

Alright, let's simulate the defuse and figure out whether it is working as expected!

let simulateDefuse char =
let seconds = 5
let t = TimeBomb()
t.Status |> printfn "Status: %A"
t.Activate(seconds, 'a')
printSecondsRemaining t
t.Status |> printfn "Status: %A"

TimeSpan.FromSeconds 3. // <1>
|> timeOut
|> Alt.afterFun (fun _ -> t.TryDefuse(char)) // <2>
|> Alt.afterJob (fun _ -> t.DeadStatusAlt)
|> Alt.afterFun (printfn "Status: %A")
|> run

The simulateDefuse function takes an input character and uses it to defuse the bomb (<2>) after a delay of three seconds (<1>).

> simulateDefuse 'a' ;;
Status: NotActivated
Status: Alive
Seconds Remaining: 4
Seconds Remaining: 3
Seconds Remaining: 2
Status: Defused
Real: 00:00:03.023, CPU: 00:00:00.026, GC gen0: 0, gen1: 0
val it : unit = ()

Cool, we made it ๐Ÿ˜ƒ

Another simulation that we can add here is putting multiple time bombs in action. I leave it as an exercise for you!

Summary

In this blog post, we learned how to manage to state mutation (or transition) in a concurrent program using the abstractions provided by Hopac.

The source code of this part is available on GitHub.


Did the content capture your interest? Stay in the loop by subscribing to the RSS feed and staying informed!