Remember, a FRP is used to model systems that have an evolution with respect to continuous time. However the way such a system computes its output(s) may be modified at discrete moments in time. To take a simple example, suppose we want to simulate a ball moving in one direction along a horizontal axis at constant speed. Suppose also that wa have an input device with two controls, the first control sets the velocity of the ball and the second is a button that, when pressed, changes the direction of the ball. Moreover, the ball is only allowed to move between 2 vertical walls. Each time the ball hits one of these walls, its velocity changes direction. This system behaves continuously until one of the three following events occur:
- The user changes speed.
- The user presses the velocity direction button.
- The ball hits a wall.
The goal of this post is to discuss such internal events.
Some let's start with our Behavior type and its run function.
#light type Time = float type 'a Beh = Beh of (Time -> ('a * 'a Beh)) let rec run (Beh bf) l = match l with |[] -> [] |h::t -> let (r, nb) = bf h r::(run nb t)I will not propose to implement the above case with the ball and the walls since this is still a bit too complex for the moment and would hide some important concepts. That is why I prefer an simpler example of a colored Behavior that is white at the beginning of its life and becomes black after some time (namely 10 units of time - i.e seconds). As usual, let's start the "hard way" by manually coding all the logic. Helpful combinators will be derived from this first coding.
type Color = White|Black // a constant Behavior that is always Black let rec blackB = Beh (fun t -> (Black, blackB)) let rec colorB = let bf t = if (t<10.0) then (White, colorB) else let rec blackB = Beh (fun t -> (Black, blackB)) (Black, blackB) Beh bfLooking at colorB, it is easy to see that it stays the same, it is always White. When the time reaches 10 seconds, colorB transforms itself into blackB.
let s = Seq.to_list (seq { for x in 1 .. 15 -> (float) x}) let r = run colorB sAs always, the name of the game is to find abstractions (that is, combinators) to help us writing such event handling logic.
The next step is to move the event detection outside of the Behavior function. This is done by defining a cond function that returns None if time is less than 10 seconds and Some blackB when time is higher than or equal to 10 seconds.
The cond function acts as a sort of event generator for colorB.
Now, the Behavior function (bf) invoques cond and tests whether it returns None of Some newBehavior. If cond returns None, then the colorB Behaviors stays the same else colorB becomes the new Behavior returns by cond (newBehavior). We can ask ourselves what we have gained here. The main advantage is that the logic of producing the event is now separated from the handling of the event.
Moreover the handling of the event is always the same and does not depends on a particular event: If cond is None, then we do nothing special if cond is Some newBehavior then we replace the current Behavior by newBehavior.
let rec colorB = let cond t = if (t<10.0) then None else let rec blackB = Beh (fun t -> (Black, blackB)) Some blackB let bf t = match cond t with |None -> (White, colorB) |Some (Beh newColorB) -> newColorB t Beh bf let r = run colorB sOne more step. Looking at cond, we see that it only depends on time. There is no particular reason to keep it defined within the definition of colorB. It could be passed as a parameter.
// val createColor : (Time -> Color Beh option) -> Color Beh let rec createColor cond = let bf t = match cond t with |None -> (White, createColor cond) |Some (Beh newColorB) -> newColorB t Beh bf // val cond : float -> Color Beh option let cond t = if (t<10.0) then None else let rec blackB = Beh (fun t -> (Black, blackB)) Some blackB let colorB = createColor cond let r = run colorB sThe next step is more fundamental. The cond function depends on time. But we already have things that depends on time, namely Behaviors. So why cond could not be a Behavior? If fact, cond can well become a Behavior. One big advantage of this is that cond functions may have internal state, just like any Behavior.
// -- cond becomes a behavior let rec condB = let bf t = if (t<10.0) then (None, condB) else let rec blackB = Beh (fun t -> (Black, blackB)) (Some blackB, condB) Beh bfAdapting createColor is rather easy.
// val createColor : Color Beh option Beh -> Color Beh let rec createColor (Beh condf) = let bf t = match condf t with |(None, ncond) -> (White, createColor ncond) |(Some (Beh newColorB), ncond) -> newColorB t Beh bf let colorB = createColor condB let r = run colorB sIt is still possible to increase the abstraction level. Up to now createColor explicitly depends on the value White. But taking a closer look to what we want, we can conclude that colorB follows the rules:
- Before some time t0 (10 seconds here) colorB is equal to a constant Behavior whose value is White.
- After that time, colorB should be equal to the Behavior blackB.
// val switchB : 'a Beh -> 'a Beh option Beh -> 'a Beh let rec switchB (Beh bfInit) (Beh condf) = let bf t = match condf t with |(None, ncond) -> let (rInit, nBInit) = bfInit t (rInit, switchB nBInit ncond) |(Some (Beh newBehavior), ncond) -> newBehavior t Beh bf let rec whiteB = Beh (fun t -> (White, whiteB)) let colorB = switchB whiteB condB let r = run colorB sIn the switchB signature, a quite complex type appears: 'a Beh option Beh . For the sake of clarity and to better discriminate between Behaviors and Events, a specific type is introduced for events:
// definition of event type 'a Event = Evt of (Time -> ('a option * 'a Event))And the switchB function now becomes:
// val switchB : 'a Beh -> 'a Beh Event -> 'a Beh let rec switchB (Beh bfInit) (Evt condf) = let bf t = match condf t with |(None, ncond) -> let (rInit, nBInit) = bfInit t (rInit, switchB nBInit ncond) |(Some (Beh newB), ncond) -> newB t Beh bfHere we are. In a next post I will present combinators and types to create various Events.
2 comments:
If, like me, you enjoyed this series only to discover someone had ripped out the last page you will be interested to find what, I assume to be, the final version here:
http://fsreactive.codeplex.com/SourceControl/latest#FsReactive/FsReactive.fs
My practice script!!
https://gist.githubusercontent.com/ingted/b1d5fcadc4cb76f89790f9f990ee55b1/raw/565a8eecad39926a7a4ddb7b547387facc0a15aa/FSharp_FRP_practice.fsx
Post a Comment