Part 1: Understanding Functional Reactive Programming
Functional Reactive Programming (or short FRP), is often mired in abstract languge and theoretical functional programming lingo. In this tutorial we will attempt to keep it simple and easy to follow.
To begin with lets define FRP. In order for something to be classified as frp it must be:
- Composable and Declarative (i.e. functional)
- Reactive
- Sampling Resolution Insensitive
The first two requirements are fairly easy to understand, i.e. of course it must be functional and be able to react to events occuring in the system. However the 3. requirement Sampling Resolution Insensitive, requires that we understand what sampling resolution is.
In short the sampling resolution is analogous to the the span of time between each sample, in video games this is often refered to as the tick rate.
However let us see how this problem plays out in an actual application. Imagine we have the following program
1: 2: |
|
This program updates its state and nothing more, the sampling resolution of this program depends entirely on computer running it and how costly the function update
is.
If we assume that it always takes 16 ms to execute update
then we get a sampling resolution at
1:
|
|
Meaning that update is called about 60 times per seconds.
However as is obvious there is no guarantees given that update
will take exactly 16 ms to execute. Which might be a problem if an application needs to update at precise interval,
old programs have sometimes ignored, and thus when executed on modern computers they run extremly fast or have completely stopped functioning properly.
The a naive solution to account for a fixed sampling rate, would be to delay after the update. e.g.
1: 2: 3: 4: 5: 6: 7: |
|
Initially this provides us with what we required, the program operates consistently at a fixed sampling rate.
However as you can probably already observe, if for any reason at any point during the life time of the program, update
takes longer than expected.
Meaning that diffTime > desiredDelay
i.e. requring us to wait negative, then the program will become out of sync forever.
If diffTime > desiredDelay
happens consistenly on average, then there is nothing we can do, from a development perspective that can be done.
This is because the resolution supported by the system is too low to support the resolution required. In the same way that if we have a picture with a high resolution, showing it on a small screen doesn't show any more details.
The resolution required and the resolution supported must always follow each other, however they need not necessarily perfectly match, which is why we must design our programs to accomondate this.
Otherwise If diffTime > desiredDelay
happens only on rare occasions, then on those occasions we will forever delay all feature updates.
e.g. Imagine a program simulating a car but with the expectation that an updateCar
function is called 10 times a second, then skipping a couple of calculations can have a huge impact on the result.
To solve the second of the two problems let us propose the another naive solution
1: 2: 3: 4: 5: 6: 7: |
|
This solution solves the problem by calling update
an extra time in the event it becomes out of sync.
So job done? Not quite... because depending on how long a single execution takes it might still cause update to become out of sync.
Imagine that update
was for a graphical application and looked something like this:
1: 2: 3: |
|
As you might imagine rendering two times in a row doesn't change anything from an application perspective, however it is a huge drain on the resources of the cpu.
So what we in reality want is something that allows us to define that if we miss an updateState
then we guarantee that we calculate it but if we miss a render
then will just be skipped.
Before we move onto how frp would handle these problems, let's highlight one thing that has been overlooked so far.
Remember the Insensitive in Sampling Resolution Insensitive, thus far the proposed solutions have completely overlooked this aspect.
All we have achieved is uniform(kinda..) sampling however consider what would happen if we changed the desiredDelay
to half its current value.
Essentially this would mean update
is called twice as many times, or in other words the application is Sampling Resolution Sensitive.
On reflection if we consider all the prior problems they have all been the victim of some form of Sampling Resolution Sensitivity. This is why Sampling Resolution Insensitivity is and must be a property of FRP.
So far the problems we have shown are
- Unpredictable Sampling
- Single samplings causing out of sync
- Redudant sampling
- The root to all of these being Sampling Resolution Sensitivity
Part 2: Using Freckle
Now that we understand the problem of Sampling Resolution Sensitivity, let's see how code using freckle would look.
Lets assume we are building a simple count app, that will count and show the result in the console.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: |
|
To set a bounded resolution we simply append a delay 100ms ~ 10 ticks per second
1: 2: 3: |
|
With this all that should be left is simply to begin sampling
1: 2: 3: |
|
Run 1
1: 2: 3: 4: 5: |
|
So far so good we see that even though we sample 10 times more times than we need our app remains consistent.
Question: what happens if the resolution is below our needs? Let's see what occurs when we set the delay to 2 seconds
1: 2: 3: |
|
Run 2
1: 2: 3: 4: 5: |
|
As we can see the prints (the call to our render function) only happens every other second. However our counting remains correct.
You could say that our application changed behavior duo to changing of the resolution, and thus is not Sampling Resolution Insensitive.
However this is from the app's perspective completely expected and counted for behavior, as it explicitly pulseUpto
for the printout.
In truth the app only cares about getting a correct count, printout is a secondary concern and it is only interested in it occuring at some point.
Thus the app maintains its property of being Insensitive
For a fun exercise try and run the code yourself!, try and see what happens if the resolution is unbounded.
Full name: Tutorial.update
union case Time.Time: unit -> Time
--------------------
type Time =
| Time of unit
static member ( - ) : 'a * 'b -> Time
Full name: Tutorial.Time
Full name: Microsoft.FSharp.Core.unit
Full name: Microsoft.FSharp.Core.Operators.failwith
Full name: Tutorial.now
Full name: Tutorial.time
Full name: Tutorial.wait
Full name: Tutorial.updateState
Full name: Tutorial.render
Full name: Tutorial.desiredDelay
Full name: Tutorial.updateState
Full name: Tutorial.render
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
union case Time.Time: unit -> Time
--------------------
type Time =
{Ticks: Ticks;}
override ToString : unit -> string
static member between : a:Time -> b:Time -> Time
static member maxValue : Time
static member origin : Time
static member ofDateTime : d:DateTime -> Time
static member ofDays : days:int32 -> Time
static member ofHours : hour:int32 -> Time
static member ofMicroseconds : microSec:int32 -> Time
static member ofMilliseconds : ms:int32 -> Time
static member ofMinutes : min:int32 -> Time
static member ofSeconds : sec:int32 -> Time
static member ( + ) : t1:Time * t2:Time -> Time
static member ( - ) : t1:Time * t2:Time -> Time
static member ticks : t:Time -> Ticks
static member time : t:Ticks -> Time
static member toDateTime : t:Time -> DateTime
Full name: Freckle.Time
Full name: Tutorial.app
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
module Sample
from Freckle
--------------------
type Sample<'a> = Period -> 'a
Full name: Freckle.Sample<_>
module Async
from Freckle
--------------------
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Full name: Freckle.ComputationExpression.sampleAsync
union case Feed.Feed: LazyList<Time * 'e> -> Feed<'e>
--------------------
module Feed
from Freckle
--------------------
type Feed<'e> =
| Feed of LazyList<Time * 'e>
override ToString : unit -> string
Full name: Freckle.Feed<_>
Full name: Freckle.Feed.pulse
Full name: Freckle.Sample.bind
Full name: Freckle.Feed.foldPast
from Freckle
Full name: Freckle.SampleAsync.ofSample
Full name: Freckle.Feed.pulseUpto
Full name: Freckle.Sample.map
Full name: Freckle.Feed.map
Full name: Freckle.Feed.plan_
Full name: Tutorial.boundResolutionApp
Full name: Freckle.SampleAsync.doAsync
Full name: Freckle.Sample.sampleForever
union case Clock.Clock: Async<Time> -> Clock
--------------------
module Clock
from Freckle.Clock
--------------------
module Clock
from Freckle
--------------------
type Clock = | Clock of Async<Time>
Full name: Freckle.Clock.Types.Clock
Full name: Freckle.Clock.Clock.systemUtc