Introducing Reactive: Behaviors
In the last post, Reactive’s Events were examined. While Events cover a lot of ground, Reactive’s Behaviors allow programmers to think beyond sampling when writing games and other interactive programs. This post introduces Behaviors and the functions that make them useful.
Semantically, Behaviors can be thought of as functions of time:

Note that, unlike Events, behaviors do not include times at
and
.
Behaviors have a value at all times. The position of a computer mouse is a good example. At any time, there is exactly one position for the mouse. Other behaviors include the data on a hard drive, the color of a pixel, and the sound from speakers.
We’ll be exploring behaviors through various implementations of a dot machine. This machine consists of a display that has the ability to show any number of dots at arbitrary locations as well as a mouse with one button used for input.
type Dot = (Double,Double) data Input = I { mouse :: Behavior (Double,Double) , button :: Event () , integral :: (VectorSpace v, Scalar v ~ TimeT) => Behavior v -> Behavior v } type DotMachine = Input -> Behavior [Dot]
Note that our Input data type also includes an integral function. We’ll assume that this performs some method of integration over time.
Simple Examples
For some basic examples, we’ll also consider machines that always display one dot.
type SingleDotMachine = Input -> Behavior Dot toDotMachine :: SingleDotMachine -> DotMachine toDotMachine = (fmap.fmap) pure -- Alt. Def -- toDotMachine s i = fmap (\a -> [a]) (s i)
Note that fmap applies to behaviors just as it applies to events.
A machine that displays a dot at the mouse location is quite simple:
atMouse :: SingleDotMachine atMouse = mouse
Now, consider a machine that displays a dot at the origin of the screen.
originBA :: SingleDotMachine originBA = const (pure (0.0,0.0)) -- Alt. Def. -- originBA = (pure.pure) zeroV
pure produces a Behavior that is constant with respect to time. In our case pure (0.0,0.0) creates a Behavior of \t -> (0.0,0.0).
In the alternate definition, we use zeroV which is a function from the vector-space library and, in our case, is a synonym for (0.0,0.0). pure, when applied to functions, is equivalent to const.
Animation
One useful built in behavior is one that simply returns the current time as a Double.
time :: Behavior Double
time allows us to build simple animations.
Lets implement a machine that orbits the origin.
-- Simple Polar to Cartesian conversion p2c :: (Floating a) => a -> a -> (a,a) p2c mag phase = (mag*cos phase, mag*sin phase) -- Orbit around the origin with a radius of 10.0 orbit :: Behavior (Double,Double) orbit = fmap (p2c 10.0) time
Behaviors go beyond fmap in that a normal function can be “lifted” to apply to behaviors.
p2cB :: (Floating a) => Behavior a -> Behavior a -> Behavior (a,a) p2cB = liftA2 p2c
The 2 in liftA2 specifies the number of parameters that should be converted to Behaviors. liftA3, and liftA4 are also available. These lift functions are defined in terms of a more general <*> operator. To learn more about this mechanism, see Applicative Functors on Wikibook.
Now we can define a machine with a spiral path:
spiral :: Behavior (Double,Double) spiral = p2cB (fmap (\t -> 10.0*sin t) time) time
Integration
Reactive’s integration functions provide us with a straightforward way to incorporate simple physics into our programs. Recall from calculus how to find the position
of an object with an initial position
, an initial velocity
, and a constant acceleration of
.



In Reactive, this would be implemented as follows:
a = pure a0 v = pure v0 ^+^ integral a p = pure p0 ^+^ integral v
There is a trick going on here. Recall that the expression pure v0 creates a constant function of time behavior. The plus (^+^) in this case actually adds two behaviors together. The definition of v could also have been written as:
v = liftA2 (^+^) (pure v0) (integral a) -- or even simpler v = fmap (^+^ v0) (integral a)
Reactive provides a vector space instance for behaviors of vectors space instances. Instances are also supplied for the various Num classes so you can feel free to use the normal + operator on two behaviors of Doubles, for example.
Consider a dot machine where the dot starts at the origin and is attracted towards the mouse. The further away the dot is, the faster it moves towards the mouse position.
First, we’ll use a general attractor function:
attract :: (Behavior Dot->Behavior Dot) -> -- The integral function Dot -> -- The initial position Behavior Dot -> -- The behavior to attract towards Behavior Dot attract int pos0 attractor = pos where pos :: Behavior Dot pos = pure pos0 ^+^ int vel vel :: Behavior Dot vel = pos ^-^ attractor
Now the dot machine implementation is quite simple.
followMouse :: SingleDotMachine followMouse i = attract (integral i) zeroV (mouse i)
Behaviors with Events
Reactive includes several functions involving both Behaviors and Events, the most primitive being stepper and snapshot.
-- | Discretely changing behavior, based on an initial value and a -- new-value event. stepper :: a -> Event a -> Behavior a -- Snapshot a behavior whenever an event occurs. snapshot :: Behavior b -> Event a -> Event (a, b) snapshot_ :: Behavior b -> Event a -> Event b
Using the above functions, we can make a machine that shows a dot at the origin until the button is pressed, in which case it will appear at the mouse position.
mouseAtPress :: Input -> Event Dot mouseAtPress i = snapshot_ (mouse i) (button i) -- Alt. Def. -- mouseAtPress = liftA2 snapshot_ mouse button wherePressed :: SingleDotMachine wherePressed = stepper zeroV . mouseAtPress
If we instead want a dot to continue being displayed, we’ll collect the events,
collectE :: Event a -> Event [a] collectE = monoidE . fmap pure -- Alt def. -- collectE = scanlE (++) [] . fmap (\a -> [a])
, make them into a behavior,
collectEB :: Event a -> Behavior [a] collectEB = stepper mempty . collectE -- Alt def. -- collectEB = stepper [] . collectE
, and put it together:
atCursor :: DotMachine atCursor = collectEB . mouseAtPress
Dynamic Collections
The atCursor example included a dynamically changing collection, a list of dots. What if instead we wanted a list of interactive behaviors instead? It turns out that this is difficult to implement in reactive as it is now. At the time of this writing reactive is being revamped for this purpose.
What’s Next
In the next article, I’ll conclude this series by taking a look at legacy adapters which enable our reactive programs to connect with the IO monad.
Acknowledgments
This tutorial was created by David Sankel with help from Thomas Davie and was sponsored by Anygma.
I know a tiny bit of Haskell but I haven’t encountered the ~ symbol before:
integral :: (VectorSpace v, Scalar v ~ TimeT) => Behavior v -> Behavior v
Could you give an URL that explains this feature? I tried to Google but “Haskell constraint ~” gives too many hits
Thanks, Peter
Peter, thanks for asking. The ~ symbol is part of type families. You can learn more about them here. Section 7.3 introduces the type equality operator. Essentially, the type of integral is communicating that integral will work on behaviors of v where TimeT is the scalar of v. In other words, a TimeT multiplied by a v (Using *^) produces a v.
The ~ means type equality, it’s part of the new type families extension. So that constraint says that the scalar field for the vector space v must be TimeT.
I’m not sure I understand the difference between behaviors and events. Is the only difference that behaviours don’t have a value at -∞ and +∞? What is the significance of that?
Michael, events occur at certain times while behaviors have a value that can be queried at any time. A behavior function, like sin t, has a value at any time t . . . there isn’t an equivalent event for it.
Shouldn’t it be v = liftA2 (+) (pure v0) (integral a) v = fmap (+ v0) (integral a) rather than v = liftA2 (^+^) (pure v0) (integral a) v = fmap (^+^ v0) (integral a) ?
Also, isn’t there a problem with (efficient) implementation of integral? It requires sampling of behaviour, for which, given that behaviour is not reactive, there are no obvious time points.
Eugene, you’re suggesting using ‘+’ instead of ‘^+^’.
^+^ is an operation on additive groups (in the vector-space library) while + is an operation on Nums from the standard prelude. For most datatypes that are additive groups and Nums, the meaning is probably the same. However, a vector type cannot be a Num (and therefore doesn’t support the + operation) because it does not have an appropriate (*) operation as well.
Yes, as you pointed out, there is a problem with an efficient implementation of integral. An even bigger problem is finding a meaning for integral that makes sense. In reactive right now, the integral actually takes another argument of type (Event ()) that is used to approximate the calculus style integral at those time points.
Thanks for your comments.
[...] continue on… [...]
Hi,
great series! please continue