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:

\mbox{\bf type } B_a = T \to a

Note that, unlike Events, behaviors do not include times at -\infty and +\infty.

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 p of an object with an initial position p_0, an initial velocity v_0, and a constant acceleration of a_0.

a(t) = a_0
v(t) = v_0 + \int_0^t a
p(t) = p_0 + \int_0^t v

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.

Comments (9)

Peter VerswyvelenDecember 3rd, 2008 at 1:17 pm

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

David SankelDecember 3rd, 2008 at 3:23 pm

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.

Cale GibbardDecember 3rd, 2008 at 3:24 pm

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.

MichaelFebruary 13th, 2009 at 4:54 am

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?

David SankelFebruary 13th, 2009 at 11:17 am

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.

EugeneJune 15th, 2009 at 6:31 am

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.

camioJune 15th, 2009 at 10:13 am

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… [...]

MathijsJuly 25th, 2009 at 1:27 pm

Hi,

great series! please continue :)

Leave a comment

Your comment