FRP (which stands for functional reactive programming) is a domain specific language (DSL) embedded in Haskell that is used for programming hybrid reactive systems. It can be further tailored to specific applications, yielding DSLs for graphics and animation (Fran), robotics (Frob), and computer vision (FVision). In this document the emphasis is on robotics.
FRP allows interaction with the outside world to be expressed in a concise, declarative manner without resorting to the IO monad. FRP is independent of the underlying application and is easy to incorporate into new interactive applications.
The ideas underlying FRP were originally developed by Conal Elliott in the context of graphics and animation []. There are, however, subtleties in the language design and implementation, and thus we refer to the version described here as Yale FRP. It is based on a stream-based implementation of FRP developed by Paul Hudak in the book The Haskell School of Expression: Learning Functional Programming through Multimedia (SOE) [], but it also includes significant library modules taken from Conal Elliott's original Fran implementation.
Yale FRP is "embedded" in the functional language Haskell. Thus on one hand it can be viewed as nothing more than a Haskell library. But Haskell's flexible type system, higher-order functions, and lazy evaluation permit a design that has the look and feel of a very different sort of language. Nevertheless, learning Haskell is a prerequisite to programming in FRP. Readers should refer to introductory Haskell material such as A Gentle Introduction to Haskell [], which is most useful to those who have already programmed in a functional language before, or to the SOE textbook []. In the interest of completeness, this manual includes a very brief overview of Haskell. Examples are drawn from animation and robotics.
Yale FRP is implemented as a Haskell library, so you must have a Haskell implementation installed before you can install FRP. There are many Haskell implementations available, all of which are free. We recommend Hugs (http://haskell.org/hugs) above others because it is easy to use, being an interactive interpreter, and because Yale FRP was developed under Hugs, so we expect this combination to be most robust. The GHC Haskell compiler (http://haskell.org/ghc) can also be used, and is critical for compute-intensive applications, but it is harder to use, and does not yet support the graphics library.
Yale FRP also uses the Haskell graphics library, which is not part of the standard Hugs distribution. It can be downloaded from http://haskell.org/graphics.
Yale FRP itself can be downloaded from http://haskell.org/frp as a gzipped file, which should be unpacked into a new directory, say ~/frp. Then add ~/frp to Hugs' search path (you know how to do this if you installed the graphics library yourself; otherwise check the Hugs user's manual).
In the remainder of this manual we will refer to Yale FRP simply as FRP, and we assume that you are using the Hugs implementation of Haskell.
Since FRP inherits the syntax, type system, and libraries of Haskell, users of FRP must, of necessity, learn about Haskell. This section contains a very brief overview Haskell; those familiar with Haskell may wish to skip to the next section.
The essential Haskell features for FRP programming include:
To help understand examples in this guide we include a brief explanation of some essential features.
Conversely, an ordinary function such as f can be coerced into an infix operator by surrounding it in backquotes, as in x `f` y, which is the same as f x y. Normal application takes precedence over infix application; for example f x+y z parses as (f x) + (y z) (and for this reason one would normally write it as f x + y z).
A type class can also have more than one type parameter. The following
class simulates the super-class relationship in Java:
class SubType a b where -- a is a sub-type of b
up :: a -> b
down :: b -> Maybe a
FRP programmers don't generally need to define new type classes or instances, but FRP does have many pre-defined classes that the user must be aware of.
Haskell provides a special module called the Prelude to define a wide variety of built-in functions and data types, some of which are used extensively in the examples in this manual. They are listed here, although some have already been discussed.
Data types:
data Integer = ... Infinite precision integer
data Int = ... 32 bit Integer
data Double = ... double prec floats
data Bool = True | False
data Char = ... a single character
type String = [Char] -- a string is just a list of chars
data [a] = [] | a : [a] -- lists
data (a,b) = (a,b) -- tuples
Functions:
fst :: (a,b) -> a -- first component of a 2-tuple
fst (x,y) = x
snd :: (a,b) -> b -- second component of a 2-tuple
snd (x,y) = y
($) :: (a -> b) -> a -> b
f $ x = f x -- infix function application
(.) :: (a -> b) -> (b -> c) -> (a -> c)
f . g = \x -> g (f x) -- composition
Types in Haskell are quite expressive; understanding and exploiting their utility is highly encouraged. Although types in Haskell are inferred, allowing type signatures to be omitted, it is standard practice to write explicit type signatures for every top-level function (as above), which aids clarity, forces one to understand well the structure of a program, and makes type errors easier to diagnose. In fact type signatures are so useful that it is sufficient for a manual such as this to describe functions by simply giving their type signatures and providing an informal description of how they behave.
In this section the basic ideas underlying FRP are introduced.
In FRP, most values evolve over time. Continuously varying values (i.e. defined at every point in time) are called behaviors. Discretely varying values (i.e. defined only at specific points in time) are called Events. A behavior or event that yields values of type a in the presence of input i has type Behavior i a or Event i a, respectively.
For example, consider a robot equipped with a set of sonars for
navigation. At regular intervals, the robot samples these sonars,
yielding a list of proximity values:
type Length = FRPReal
... :: [Length] -- a list of distance values
where FRPReal defines a generic floating point type (usually
Double). Rather than deal with these sonar samples
individually, Frob captures them as a stream of discrete values;
specifically, as an event of type Sonar i => Event i [Length], where
i is the "input" to the overall process: a record that
contains the readings of all of the robot's sonars (and possibly other
sensors). The constraint Sonar i says: whatever i is,
it has to contain sonar readings.
sonarReadings :: Sonar i => Event i [Length]
Event values are available only at specific points in time.
Behaviors, however, are defined at all points in time. Conversion
from discrete to continuous time requires interpolation. The
simplest interpolation scheme is "sample and hold": that is, return
the value of the most recent event. The stepB function
implements this sample-and-hold strategy:
stepB :: a -> Event i a -> Behavior i a
Since behaviors have value at all times, we can compose them
pointwise, a property that events do not hold in general:
vl, vr :: Behavior Sensors Vector2
-- velocities of left and right wheels of a differential drive robot
vc :: Behavior Sensors Vector2
-- velocity of the center of the robot
vc = (vl + vr)/2
In this example, +, / and 2 are functions and
constants in the behavior world. Haskell type classes allow us to use
the same symbol (e.g. +) for both the static version and the
behavioral version of these values.
In general, we cannot compose two events pointwise, because they are
not guaranteed to always occur at the same discrete times. This is
annoying, so Frob also defines clocked events of type
CEvent clock i a. The point is, if two clocked events
e1 and e2 have the same clock type, then they are
guaranteed to be synchronous, i.e. e1 occurs whenever e2
occurs. This useful property allow us to safely apply lifted
functions to synchronous events. For example,
leftSonarE, rightSonarE :: Sonar i => CEvent SonarClock i Length
leftSonarE = ... -- reading of the left sonar, i.e. distance to left wall
rightSonarE = ... -- reading of the right sonar, i.e. distance to right wall
pathWidthE :: Sonar i => CEvent SonarClock i Length
pathWidthE = leftSonarE + rightSonarE
-- width of the path ((+) is lifted here)
Haskell's type system prevents events with different clocks to be
mixed, so the following code generates a type error:
frontSonarE :: Sonar i => CEvent SonarClock i Length
bumperE :: Bumper i => CEvent BumperClock i Bool
e = bumperE ||* (frontSonarE <=* 1.0) -- error!
FRP is about reactive programming, so signals need to be able to
react to other signals. Switching is one way to do this.
switch :: Behavior i a -> Event i (Behavior i a) -> Behavior i a
From the type signature of switch we note that the second
argument is an event that generates behavior values, something we
haven't seen before.
Given a behavior b defined as:
b = b0 `switch` e
b will start as b0, and when e occurs with
behavior b1, b will behave like b1; later when
e occurs and generates another behavior b2, b
will switch to b2, and so on.
But how do we get something of type Event i (Behavior i a) to
start with? By using the event mapping operator:
(==>) :: Event i a -> (a -> b) -> Event i b
The two arguments to (==>) are an event and a function. What
(==>) does is to apply the function argument to each occurrence
of the event argument. If b is a behavior type, then the
result is an event of behaviors.
For a concrete example, suppose we have a synchronous drive robot for
which we can only control the steering rate of the wheels (the velocity
is fixed), and the robot senses the environment via an array of sonars
(the sonarReadings event). Our goal is to try not to crash the robot,
so we need a function that generates the right turning rate behavior
given the most recent sonar reading:
type AngSpeed = FRPReal -- angular speed
avoidObstacle :: Sonar i => [Length] -> Behavior i AngSpeed
avoidObstacle sonars = ...
In the following example, the robot goes straight at first, and then
calls avoidObstacle to determine the right turning rate every
time a set of sonar readings is available:
wander :: Sonar i => Behavior i AngSpeed
wander = 0 -- Don't turn wheels initially;
`switch` sonarReadings -- If new readings come,
==> avoidObstacle -- use the most recent sonar
-- readings to calculate the
-- desired turning rate
Sometimes we only want to switch the behavior on the first
occurrence of the event. For this purpose we can use till:
till :: Behavior i a -> Event i (Behavior i a) -> Behavior i a
b `till` e = b `switch` onceE e
onceE e keeps the first occurrence of event e but drops
the rest of them.
One thing to remember is that when we switch into a behavior, that behavior is not started until the switching. But why should we care about when a hehavior is started? That's because some behaviors rely on the past (we say they are stateful in this case) and therefore can be affected by the start time. As an example, consider a robot that does not have an odometer but has a sensor for wheel velocity. We can define the odometer behavior as the integral of the wheel velocity. It is important to know when the integration begins, because all distance travelled before the integration will be ignored.
We would also like to be able to switch events, so switch and
till are overloaded to handle events as well:
switch :: Event i a -> Event i (Event i a) -> Event i a
till :: Event i a -> Event i (Event i a) -> Event i a
FRP largely consists of the following modules:
To use FRP, import the appropriate top-level module:
As you gain more experience with Haskell, you are encouraged to read the FRP source files, which no doubt will deepen your understanding of the system. Besides, the source code is (almost) always the best documentation.
The best way to learn programming is to actually program, and we're
sure you just can't wait to see FRP in action, so let's get started.
First, create a file Hello.hs with the following content:
import SimuGraphicsFRP
main = teximate_ $ constB "Hello, world!"
Then fire up Hugs with the -98 switch, which allows some non-standard
features of Haskell to be used and is required to run FRP:
$ hugs -98 Hello
Now your program should be properly loaded. If you see any error message, make sure the directory where Hugs is intalled is in your search path and the file Hello.hs is in the current directory.
Next type main<return> at the Hugs prompt:
Main> main
and you should see a screen full of the text "Hello, world!".
Congratulations! You have just successfully run your first FRP program!
Although this example is not very sophisticated, soon you will be able
to write more powerful and interesting application in FRP.
To interrupt the execution, simply press Control-C. Then you can type ":q" to quit Hugs.
In this section you will find the definition of the FRP language. You are not expected to read this part line by line. Instead, skim it and come back later when need arises.
Types in FRP are divided into static types and signal types. The former are for values that do not change over time, and the latter are for behaviors and events.
-- static types
type FRPReal = Double
type Time = FRPReal -- in seconds
type DeltaTime = FRPReal -- Time deltas, i.e. durations
type Angle = FRPReal -- in radians
data Point2 = Point2XY FRPReal FRPReal
data Point3 = Point3XYZ FRPReal FRPReal FRPReal
data Vector2 = Vector2XY FRPReal FRPReal
data Vector3 = Vector3XYZ FRPReal FRPReal FRPReal
data Transform2 = ... 2D Transformation Function
data Transform3 = ... 3D Transformation Function
-- signal types
data Behavior input a = ...
data Event input a = ...
data CEvent clock input a = ...
Events and clocked events are collectively called generic
events, a concept captured by the GEvent class:
instance GEvent Event
instance GEvent (CEvent clock)
FRP distinguishes itself from most other reactive programming
languages by having first-class time-varying values (signals). Static
functions may be lifted into the signal domain, yielding a function
that operates pointwise on its behavior arguments to return a behavior
result. In FRP there are a family of functions that perform this
lifting operation, one for each function arity. Hence the following
type class:
class Functor z => Zippable z where
($*) :: z (a -> b) -> z a -> z b
lift0 :: a -> z a
lift1 :: (a -> b) -> z a -> z b
lift2 :: (a -> b -> c) -> z a -> z b -> z c
lift3 :: (a -> b -> c -> d) -> z a -> z b -> z c -> z d
lift4 :: (a -> b -> c -> d -> e) -> z a -> z b -> z c -> z d -> z e
-- and so on
The name Zippable is picked because lifted functions "zip"
together different signals. There are two instances of
Zippable:
instance Zippable (Behavior input)
instance Zippable (CEvent clock input)
Most numeric functions are available in lifted form via overloading. Just use the same function name for continuous behaviors that you use in the static world. In particular, all methods in the classes Num, Franctional, Floating, and Integral are overloaded to work on behaviors (see the Haskell 98 Report for a list of these methods). Examples: ...
Many pre-defined Haskell functions, however, are not methods in existing classes, and thus FRP defines its own lifted versions of them. By convention, the suffix Z denotes a lifted function, and the suffix * denotes a lifted infix operator.
-- 2-tuples (pairs)
pairZ :: Zippable z => z a -> z b -> z (a,b)
fstZ :: Zippable z => z (a,b) -> z a
sndZ :: Zippable z => z (a,b) -> z b
pairZSplit :: Zippable z => z (a,b) -> (z a, z b)
-- 3-tuples
tripleZ :: Zippable z => z a -> z b -> z c -> z (a,b,c)
fst3Z :: Zippable z => z (a,b,c) -> z a
snd3Z :: Zippable z => z (a,b,c) -> z b
thd3Z :: Zippable z => z (a,b,c) -> z c
tripleZSplit :: Zippable z => z (a,b,c) -> (z a, z b, z c)
-- lists
nilZ :: Zippable z => z [a]
consZ :: Zippable z => z a -> z [a] -> z [a]
headZ :: Zippable z => z [a] -> z a
tailZ :: Zippable z => z [a] -> z [a]
nullZ :: Zippable z => z [a] -> z Bool
(!!*) :: Zippable z => z [a] -> z Int -> z a
zListToListZ :: Zippable z => [z a] -> z [a]
liftL :: Zippable z => ([a] -> b) -> ([z a] -> z b)
liftL f ls = lift1 f (zListToListZ ls)
-- relational and boolean operators
(>*), (<*) :: (Ord a, Zippable z) => z a -> z a -> z Bool
(<=*), (>=*) :: (Ord a, Zippable z) => z a -> z a -> z Bool
(&&*), (||*) :: Zippable z => z Bool -> z Bool -> z Bool
(==*), (/=*) :: (Eq a, Zippable z) => z a -> z a -> z Bool
-- the Maybe type
nothingZ :: Zippable z => z (Maybe a)
justZ :: Zippable z => z a -> z (Maybe a)
maybeZ :: Zippable z => z a -> z (b -> a) -> z (Maybe b) -> z a
-- the Show class
showZ :: (Show a, Zippable z) => z a -> z String
-- String
(++*) :: Zippable z => z String -> z String -> z String
-- Boolean
trueZ :: Zippable z => z Bool
falseZ :: Zippable z => z Bool
notZ :: Zippable z => z Bool -> z Bool
ifZ :: Zippable z => z Bool -> z a -> z a -> z a
These primitives behaviors relate to the current time:
timeB :: Behavior i Time -- the global time
ltimeB :: Behavior i Time -- the local time
The global time at the start of the FRP program is always 0. The
local time starts from 0 when the current behavior or event begins.
constB turns a static value into a behavior:
constB :: a -> Behavior i a
constB = lift0
A behavior transformer changes the value of a behavior:
unzipB :: Behavior i (a,b) -> (Behavior i a, Behavior i b)
withTimeB :: Behavior i a -> Behavior i (a, Time)
These functions shape behaviors in response to events:
switch :: GEvent e => Behavior i a -> e i (Behavior i a) -> Behavior i a
iSwitch :: GEvent e => Behavior i a -> e i (Behavior i a) -> Behavior i a
till :: GEvent e => Behavior i a -> e i (Behavior i a) -> Behavior i a
a `till` e = a `switch` onceE e
iTill :: GEvent e => Behavior i a -> e i (Behavior i a) -> Behavior i a
a `iTill` e = a `iSwitch` onceE e
switchAccumB :: GEvent e =>
Behavior i a -> e i (Behavior i (a -> a)) -> Behavior i a
iSwitchAccumB :: GEvent e =>
Behavior i a -> e i (Behavior i (a -> a)) -> Behavior i a
stepB :: GEvent e => a -> e i a -> Behavior i a
x0 `stepB` e = constB x0 `switch` e ==> constB
iStepB :: GEvent e => a -> e i a -> Behavior i a
x0 `iStepB` e = constB x0 `iSwitch` e ==> constB
stepAccumB :: GEvent e => a -> e i (a->a) -> Behavior i a
iStepAccumB :: GEvent e => a -> e i (a->a) -> Behavior i a
You might have noticed each of the combinators in this group comes in two flavors, once with the "i" prefix and one without. The difference is rather subtle. Usually you only need to use the one without the "i" prefix.
These functions return the integral or derivative of a behavior. The
VectorSpace class is defined in 6.1.1, and
includes the type FRPReal, Vector2 and Vector3.
integralB :: VectorSpace v => Behavior i v -> Behavior i v
derivativeB :: VectorSpace v => Behavior i v -> Behavior i v
The type Event i a denotes a series of event occurances, each
producing a value of type a. The clocked event type
data Clock c i => CEvent c i a = ...
is the same as Event i a, except that it's associated with a
clock type, which uniquely determines the occurrence pattern of all
events of this clock type. The important thing to remember is that
same clock type means same occurrence pattern.
In this section, we examine event stream operations. You will notice that some functions work with both Event and CEvent (those with context GEvent e => ...) and some only work with Event. That's because not all operations preserve the occurrence pattern of an event.
These are the basic event generators:
neverE :: Event i a
constE :: Time -> a -> Event i a
alarmE :: Time -> DeltaTime -> Event i ()
timeIsE :: Time -> Event i ()
timeIsE t = constE t ()
The neverE event is the never occuring event. The constE function returns an event stream with a single event at the specified time and with the given value. The alarmE function defines a regularly occuring event.
Continuous behaviors may be converted into events by the following
functions:
whenE :: Behavior i Bool -> Event i ()
isTrueE :: Behavior i Bool -> Event i ()
Both events occur when the behavior changes from False to
True. If the behavior is True from the beginning, then
whenE does not have an occurrence while isTrueE does.
This is best illustrated by the example:
e1 = whenE (timeB >=* 1) -- occurs when the gloable time exceeds 1,
-- but not if the start time of e1 is
-- >= 1.
e2 = isTrueE (timeB >=* 1) -- occurs when the gloable time exceeds 1,
-- or when e2 is started if the start time
-- is >= 1.
Events obtained using these functions are called predicate events.
A snapshot captures the value of a continuous behavior at the time of
an event occurance.
snapshotE :: GEvent e => e i a -> Behavior i b -> e i (a, b)
snapshotE_ :: GEvent e => e i a -> Behavior i b -> e i b
e `snapshotE_` b = e `snapshotE` b ==> snd
An event transformer changes the value associated with an event.
(==>) :: Event i a -> (a -> b) -> Event i b
(-=>) :: Event i a -> b -> Event i b
e -=> x = e ==> const x
tagE :: GEvent e => e i a -> [b] -> e i (a,b)
tagE_ :: GEvent e => e i a -> [b] -> e i b
e `tagE_` bs = (e `tagE` bs) ==> snd
withTimeE :: GEvent e => e i a -> e i (a, Time)
withTimeE e = e `snapshotE` timeB
timeOfE :: GEvent e => e i a -> e i Time
timeOfE e = e `snapshotE_` timeB
unzipE :: GEvent e => e i (a,b) -> (e i a, e i b)
unzipE e = (e ==> fst, e ==> snd)
The tagE functions pair event occurrences with elements of a list. The list must have enough elements to match the occurrences, or a run-time error will be generated. The withPrevE functions saves the value generated by the prior occurance and pairs it with the current value.
As with the behavioral form of switchers, these functions also switch
(clocked) event streams:
switch :: GEvent e => Event i a -> e i (Event i a) -> Event i a
iSwitch :: GEvent e => Event i a -> e i (Event i a) -> Event i a
till :: GEvent e => Event i a -> e i (Event i a) -> Event i a
iTill :: GEvent e => Event i a -> e i (Event i a) -> Event i a
switch :: GEvent e => CEvent c i a -> e i (CEvent c i a) -> CEvent c i a
till :: GEvent e => CEvent c i a -> e i (CEvent c i a) -> CEvent c i a
These functions filter event streams, possibly removing occurances.
Some of these also alter the values in the event stream.
filterE :: GEvent e => (a -> Bool) -> e i a -> Event i a
filterByE :: GEvent e => (a -> Maybe b) -> e i a -> Event i b
onceE :: GEvent e => e i a -> Event i a
These functions combine event streams.
mergeE :: (GEvent e1, GEvent e2) =>
(a -> a -> a) -> e1 i a -> e2 i a -> Event i a
(.|.) :: (GEvent e1, GEvent e2) => e1 i a -> e2 i a -> Event i a
mergeCE :: (a -> a -> a) ->
CEvent c1 i a -> CEvent c2 i a -> CEvent (Either c1 c2) i a
andThenE :: (GEvent e1, GEvent e2) =>
e1 i a -> (a -> e2 i b) -> Event i b
andThenE_ :: (GEvent e1, GEvent e2) =>
e1 i a -> e2 i b -> Event i b
e1 `andThenE_` e2 = e1 `andThenE` const e2
bothE :: (GEvent e1, GEvent e2) =>
e1 i a -> e2 i b -> Event i (a,b)
The .|. operator merges two event streams; if both streams generate
an event at the same time either value may be chosen. The andThen_
function returns events in the second stream that are after the
initial event in the first stream. For example,
atPlace p1 `andThen_` atPlace p2
happens when first atPlace p1 happens followed by
atPlace p2. bothE is similar, but occurs once only when
both arguments have occurred.
These are "stream-based" event functions:
delayE :: a -> Event i a -> Event i a
delay1E :: GEvent e => e i a -> e i a
delayCE :: Clock c i => a -> CEvent c i a -> CEvent c i a
newHeadE :: GEvent e => a -> e i a -> e i a
scanE :: GEvent e => (a -> b -> a) -> a -> e i b -> e i a
withPrevE :: GEvent e => a -> e i a -> e i (a,a)
accumE :: GEvent e => a -> e i (a -> a) -> e i a
Here are something special to clocked events:
toEvent :: GEvent e => e i a -> Event i a
clockE :: Clock c i => CEvent c i ()
clockOfE :: Clock c i => CEvent c i a -> CEvent c i ()
clockOfE _ = clockE
toEvent converts a clocked event to an ordinary event. clockE and clockOfE return the clock associated with a clock type and that of a clocked event, respectively. The Clock c i => ... context ensures that the clock type c does have a clock.
In
a = b `till` c -=> d
the behavior d only becomes alive
when c occurs, so d does not see anything that happened
before c occurs. Now suppose d has memory on the
history (e.g. d is the odometer of a robot, and is defined as
the integral of the robot velocity), then we may want d to
start together with b (such that the odometer is not reset to 0
with c). This is achieved by making d a running
behavior:
runningIn :: Behavior i a -> (Behavior i a -> Behavior i b) ->
Behavior i b
a = d `runningIn` \d' ->
b `till` c -=> d'
The code reads: d is started (running) side by side with b `till` c -=> d', and is bound to variable d', such that when c occurs, we switch into a behavior that had been started earlier.
Perhaps it's easier to understand
foo `runningIn` \b -> bar
as syntactic sugar for
letrun b = foo in bar
We can not really use the letrun syntax though, because Haskell does
not allow us to define constructs that introduce binding.
runningIn also works with events and clocked events. In fact,
its exact type is:
runningIn :: RunningIn a b i => a -> (a -> b i x) -> b i x
instance RunningIn (Behavior i a) Behavior i
instance RunningIn (Behavior i a) Event i
instance RunningIn (Behavior i a) (CEvent c) i
instance RunningIn (Event i a) Behavior i
instance RunningIn (Event i a) Event i
instance RunningIn (Event i e) (CEvent c) i
The first argument of runningIn can also be tuples or lists:
instance (RunningIn a x i, RunningIn b x i) =>
RunningIn (a,b) x i
instance (RunningIn a x i, RunningIn b x i, RunningIn c x i) =>
RunningIn (a,b,c) x i
instance (RunningIn a x i, RunningIn b x i,
RunningIn c x i, RunningIn d x i) =>
RunningIn (a,b,c,d) x i
instance RunningIn a x i => RunningIn [a] x i
This means we can rewrite
b `runningIn` \b' ->
d `runningIn` \d' ->
e `runningIn` \e' ->
...
to the more concise version:
(b,d,e) `runningIn` \(b',d',e') ->
...
We have seen how to make a behavior reactive to some event:
a = b `till` c -=> d
Here, a behaves as b until c occurs, then it
behaves as d. This is known as Continuation Passing
Style.
Sometimes we would like to express "be like this until foo happens" without specifying what to be next. This concept is called task. A task combines a behavior and a terminating event, and can be composed either sequentially or in parallel. By wise use of tasks, we can achieve very structured code.
Suppose we are to write a robot controller behavior which does the following:
Go forward until stuck, then back up a little bit, make a left turn, and repeat the action sequence.
In the continuation-passing style, we can code this in a monolithic piece without using tasks, but the code will be much more modular and reusable if we divide the work into several small tasks:
We can define these small tasks independently, and then compose them in the way we want. When you write a large FRP program, it is recommended to use tasks as your basic building blocks.
Task is a very generic concept. We can imagine many different types of tasks: some may carry an internal state, some may have access to an environment, some may be able to send messages, and etc. Naturally, these different types of tasks may require different implementation. Hence, instead of having a single Task data type, we define a GTask (generic task) class, and then provide a bounch of instances of that class (remember that a class in Haskell is just an interface, and instances of a class are types or type constructors).
The mkTask function is the basic building block for tasks.
The liftT function creates a task without an exit event.
mkTask :: GTask t => Behavior i b -> Event i x -> t i b x
liftT :: GTask t => Behavior i b -> t i b x
liftT b = mkTask b neverE
We can see that in a task type t i b x, b is the
behavior result type, x is the event result type, and i
is the input type for both the behavior and the event.
Tasks are sequenced using >>= or >>:
(>>=) :: GTask t => t i a b -> (a -> t i a c) -> t i a c
(>>) :: GTask t => t i a b -> t i a c -> t i a c
t1 >> t2 = t1 >>= (const t2)
However, these operations are more commonly written out using
do notation:
do r <- t1;
t2
is the same as t1 >>= (\r -> t2). The indentation here is
important -- the r and t2 must be in the same column
(unless explicit braces are used after the do). Similarly,
do t1
t2
is the same as t1 >> t2. Of course, do notation allows
more than two tasks to be written in sequence.
The return function defines an empty task:
return :: GTask t => b -> t i a b
The task defined by return immediately exits with the specified
value.
Recursion and case statements can also be mixed in, allowing
arbitrarily complex task sequencing structures, such as this one to
repeat a task until it returns False:
repeatT :: GTask t => t i a Bool -> t i a ()
repeatT t = do r <- t
if r then repeatT t
else return ()
This function repeats a task forever:
foreverT :: GTask t => t i a e -> t i a e'
foreverT t = ft where ft = t >> ft
The bmapT function modifies the behavior in a task:
bmapT :: GTask t => (Behavior i a -> Behavior i b) ->
t i a x -> t i b x
The fmap function modifies the result of a task:
fmap :: GTask t => (x -> y) -> t i b x -> t i b y
fmap f t = do r <- r; return (f r)
This function runs a behavior in parallel with a task, adding a
snapshot of the behavior value at the end of the task to the task
result.
snapshotT :: GTask t => Behavior i a -> t i b x -> t i b (x, a)
Sometimes we want to sample a behavior and return immediately.
snapshotNowT does this for us:
snapshotNowT :: GTask t => Behavior i b -> t i a b
It does not always make sense, or is possible, to interrupt a task. Hence we have a subclass of GTask for those tasks capable of being interruptted. This subclass is named InterruptTask.
The following function changes the termination event of a task. The overall
task ends when either the old or the new termination event occurs.
The new event is parameterized over the behavior in the task. This is
usually used to add termination to an non-terminating task, but can
also be used to interrupt a terminating task.
tillT :: InterruptTask t => t i b x ->
(Behavior i b -> Event i x) ->
t i b x
When the new termination event does not depend on the behavior in the
task, we prefer to use tillT_:
tillT_ :: InterruptTask t => t i b x -> Event i x -> t i b x
t `tillT_` e = t `tillT` const e
We can limit the time a task can run:
timeLimitT :: InterruptTask t => DeltaTime -> t i a e -> t i a (Maybe e)
timeLimitT_ :: InterruptTask t => DeltaTime -> t i a e -> t i a ()
timeLimitT_ dt t = timeLimitT dt t >> return ()
Consider a robot with more than one control, for example both wheel
controls and camera controls. If we have tasks for each individual
control, how can we combine these tasks into a single controlling
task? Note that in the behavioral world, this problem is trivial:
behaviors run naturally in parallel. For tasks, though, such a
parallel combination becomes trickier. If we combine two tasks, when
does the combined task terminate? When the first subtask terminates?
What value is returned by the combined task? We resolve these
difficulties by terminating the overall task when either subtask
terminates, returning the value of the event occurring first. Same as
for tillT, we expect (|||) to work only for a subclass
of GTask:
(|||) :: ParallelTask t => t i a x -> t i b y -> t i (a, b) (Either x y)
Here Either is a predefined type:
data Either a b = Left a | Right b
t1 ||| t2 returns Left x if t1 ends first with value x, or Right y if t2 ends first with y.
Some task types support message sending. This functionality is
captured by the MsgTask subclass. This class has one method
for sending messages:
class GTask t => MsgTask t m where
sendT :: m -> t i b ()
sendT sends the message and terminates immediately.
Some tasks have mutable internal states, which can be accessed by:
getState :: t i b s
setState :: (s -> s) -> t i b s
getState is an instantaneous task that returns the current state. setState applies a state update function to the current state, and returns the state before the update.
By packaging an RController with a terminating event, we get a
RobotTask:
type RobotTask s i o e = ...
RobotTask is an instance of InterruptTask,
ParallelTask and MsgTask, and is stateful. The s
in the RobotTask is the internal state of the task, and
e is the terminating event type.
The simplest tasks are infinite behaviors: these never terminate. The
liftT function turns a behavior into a task. For example,
given a differential drive robot, for which we can control the wheel
velocity and the turning rate, a task to stop the robot can be defined
as follows:
stopT :: WheelControl o => RobotTask s i o e
stopT = liftT $ setWheelSpeeds (lift0 (0,0))
Generally, tasks will eventually terminate. The mkTask function
couples a behavior with a terminating event. This
function creates a task that proceeds forward until stuck:
forwardUntilStuckT :: (StuckDetection i, WheelControl o) => RobotTask s i o ()
forwardUntilStuckT = mkTask
(setWheelSpeeds $ lift0 (1,1))
stuck
The primitive robot task noCommandT is an "empty" task that
never sends any command to the robot:
noCommandT :: RobotTask s i o e
noCommandT does not terminate.
This task does nothing until the robot gets stuck, then it sends out a
message:
stuckOuchT :: (StuckDetection i) => RobotTask s i o ()
stuckOuchT = do noCommandT `tillT_` stuck
sendT "Ouch!\n"
Since stuckOuchT never sends any command to the robot, the
robot will not move and therefore will not get stuck. Hence
stuckOuchT by itself is not very useful. However we can run it
in parallel with other tasks that do send out commands:
forwardT :: WheelControl o => Behavior i Speed -> RobotTask s i o a
forwardT v = liftT $ setWheelSpeeds (pairZ v v)
forwardOuchT :: (StuckDetection i, WheelControl o) => RobotTask s i o ()
forwardOuchT = forwardT 1 |.| stuckOuchT
Here |.| is an easy way to construct parallel robot task:
(|.|) :: RobotTask s i o e -> RobotTask s i o e -> RobotTask s i o e
forwardOuchT consists of two sub-tasks: forwardT 1 goes forward at 1 meter/second, and stuckOuchT says "Ouch!" and terminates when the robot hits something. forwardOuchT does the two at the same time.
This task goes forward until stuck, and then returns the distance travelled:
import qualified GeometryStatic as S
measureDistanceT :: (StuckDetection i, Odometry i, WheelControl o)
=> RobotTask s i o Length
measureDistanceT = do p0 <- snapshotNowT position -- where I started
forwardUntilStuckT -- proceed until stuck
p <- snapshotNowT position -- where I am now
let d = S.magnitude (p S..-. p0)
-- calculate the distance
return d -- and return it
RobotTask can have an internal state, but so far we haven't
used it. The state can be of any type you choose. To read the
current state, use getState. To change it, use
setState. Here is an example which counts the number of times
the robot hits something:
hitCountT :: (StuckDetection i) => RobotTask Int i o e
hitCountT = foreverT $ do stuckOuchT -- when stuck, say "Ouch!"
setState (+(1 :: Int))
-- increment the counter
(count :: Int) <- getState
sendT $ "Can't believe it! I've been hit "
++ show count
++ " time(s) today!\n"
-- complain about bad luck
You might have noticed the use of explicit type signatures in
getState and setState. That's because Haskell can not
decide the type of these expressions in this case.
We can watch behaviors or (clocked) events:
watchB :: (Time -> a -> String) -> Behavior i a -> Behavior i a
watchE :: GEvent e => (Time -> a -> String) -> e i a -> e i a
Whenever the behavior's value is updated, or the event has an
occurrence, that value will be displayed using the function we supply
to watchB or watchE. Examples:
distanceB :: Event Sensors FRPReal
bumperE :: Event Sensors ()
... watchB (\t a -> show a ++ "\t") distanceB ... -- ignore the time
... watchE (\t a -> "Bump at time " ++ show t ++ "!\t") bumperE ...
To debug tasks, we have the sayT function, which defines a task that
prints to the console and immediately exits.
sayT :: String -> Task i a ()
sayT s = trace s $ return ()
Features described in this section are generally intended for power users, and can be dangerous (e.g. degrading performance or screwing up heap) falling into the wrong hands. However, sometimes they are very useful. Proceed with caution.
memoB :: Behavior i a -> Behavior i a
memoE :: Event i a -> Event i a
behaviorToEvent :: Behavior i (Maybe a) -> Event i a
eventToBehavior :: Event i a -> Behavior i (Maybe a)
withBackgroundB :: GEvent e => a -> e i a -> Behavior i a
Functions in this group allow us to compose signals as input-output transformers.
The (~>>) (read "compose") operator feeds the output of its
first argument to the second argument. (~>>) is overloaded:
(~>>) :: Behavior a b -> Behavior b c -> Behavior a c
(~>>) :: Behavior a b -> Event b c -> Event a c
comapB and comapE change the input type of signals:
comapB :: (a -> b) -> Behavior b c -> Behavior a c
comapE :: (a -> b) -> Event b c -> Event a c
inputB and inputE allow us to observe the input of a
signal directly:
inputB :: Behavior i i
inputE :: Event (Maybe i) i
Since geometry is essential in many reactive systems (e.g. animation
and robotics), FRP provides a geometry library for basic 2D and 3D
geometrical operations. All of the operations described here are
lifted, unless otherwise noted. The unlifted operations can be
obtained from the static types module (GeometryStatic),
conventionally
imported using
import qualified GeometryStatic as S
allowing unlifted operations to be accessed using a S. prefix,
as in S.point2XY. Some operations (e.g.
point2XYCoordsTuple) only have lifted version, and they are noted in
the
comments.
A number of simple naming conventions are used. In operators, vector operands are indicated by the ^ symbol and point operands by the . symbol. Thus, .-. is an operation to subtract two points. Similarly, .+^ adds a point and a vector. When we can not overload an operator for both 2D and 3D operations, the # symbol is suffixed for the 3D operation, as in (.-.) and (.-.#).
The Num class handles some operations: along with ^+^, we can use an ordinary + for vector addition. Num is defined in Haskell Prelude, so please refer to the Haskell 98 Report for a complete list of operations in the class.
Transformations are represented as a distinct datatype and are applied to objects using %$ (for 2D) or %$# (for 3D). Composite transformation are created by combining basic transformation functions with compose2 or compose3.
-- 2D Vectors
xVector2, yVector2 :: Zippable z => z Vector2 -- unit vectors
vector2XY :: Zippable z => z FRPReal -> z FRPReal -> z
Vector2
Vector2XY :: pattern constructor -- static version only
vector2Polar :: Zippable z => z FRPReal -> z Angle -> z Vector2
vector2XYCoords :: Zippable z => z Vector2 -> z (FRPReal, FRPReal)
vector2XYCoordsTuple :: Zippable z => z Vector2 -> (z FRPReal, z
FRPReal)
-- lifted version only
vector2XCoord :: Zippable z => z Vector2 -> z FRPReal
vector2YCoord :: Zippable z => z Vector2 -> z FRPReal
vector2PolarCoords :: Zippable z => z Vector2 -> z (FRPReal, Angle)
vector2PolarCoordsTuple :: Zippable z => z Vector2 -> (z FRPReal, z Angle)
-- lifted version only
rotateVector2 :: Zippable z => z Angle -> z Vector2 -> z Vector2
instance Num Vector2 -- fromInteger, * not allowed
instance Zippable z => Num (z Vector2) -- fromInteger, * not allowed
-- 2D Points
origin2 :: Zippable z => z Point2
point2XY :: Zippable z => z FRPReal -> z FRPReal -> z
Point2
Point2XY :: pattern constructor -- static version only
point2Polar :: Zippable z => z FRPReal -> z Angle -> z Point2
point2XYCoords :: Zippable z => z Point2 -> z (FRPReal, FRPReal)
point2XYCoordsTuple :: Zippable z => z Point2 -> (z FRPReal, z
FRPReal)
-- lifted version only
point2XCoord :: Zippable z => z Point2 -> z FRPReal
point2YCoord :: Zippable z => z Point2 -> z FRPReal
point2PolarCoords :: Zippable z => z Point2 -> z (FRPReal, Angle)
point2PolarCoordsTuple :: Zippable z => z Point2 -> (z FRPReal, z Angle)
-- lifted version only
distance2 :: Zippable z => z Point2 -> z Point2 -> z FRPReal
distance2Squared :: Zippable z => z Point2 -> z Point2 -> z FRPReal
linearInterpolate2 :: Zippable z => z Point2 -> z Point2 -> z FRPReal
-> z Point2
point2ToVector2 :: Zippable z => z Point2 -> z Vector2
vector2ToPoint2 :: Zippable z => z Vector2 -> z Point2
(.+^), (.-^) :: Zippable z => z Point2 -> z Vector2 -> z Point2
(.-.) :: Zippable z => z Point2 -> z Point2 -> z Vector2
-- VectorSpace class
zeroVector :: (Zippable z, VectorSpace v) => z v
(*^) :: (Zippable z, VectorSpace v) => z FRPReal -> z v -> z v
(^/) :: (Zippable z, VectorSpace v) => z v -> z FRPReal -> v
(^+^),(^-^) :: (Zippable z, VectorSpace v) => z v -> z v -> z v
dot :: (Zippable z, VectorSpace v) => z v -> z v -> z FRPReal
magnitude :: (Zippable z, VectorSpace v) => z v -> z FRPReal
magnitudeSquared :: (Zippable z, VectorSpace v) => z v -> z FRPReal
normalize :: (Zippable z, VectorSpace v) => z v -> z v
-- 1D Vector Space
instance VectorSpace FRPReal
-- 2D Vector Space
instance VectorSpace Vector2
-- 2D Transformations
mkTransform2 :: Zippable z => z FRPReal -> z FRPReal -> z FRPReal
-> z FRPReal -> z FRPReal -> z FRPReal
-> z Transform2
transform2Matrix :: Zippable z => z Transform2
-> z (FRPReal,FRPReal,FRPReal,
FRPReal,FRPReal,FRPReal)
identity2 :: Zippable z => z Transform2
translate2 :: Zippable z => z Vector2 -> z Transform2
rotate2 :: Zippable z => z Angle -> z Transform2
compose2 :: Zippable z => z Transform2 -> z Transform2
-> z Transform2
scaleXY2 :: Zippable z => z FRPReal -> z FRPReal -> z Transform2
uscale2 :: Zippable z => z FRPReal -> z Transform2
mirrorX2 :: Zippable z => z Transform2
mirrorY2 :: Zippable z => z Transform2
mirrorXY2 :: Zippable z => z Transform2
inverse2 :: (Zippable z, Monad m) => z Transform2 -> z (m Transform2)
class Tranformable2 a where
(%$) :: Transform2 -> a -> a -- Applies a transform
instance Transformable2 Point2
instance Transformable2 Vector2
class Tranformable2Z a where
(%$) :: Zippable z => z Transform2 -> z a -> z a -- Applies a
transform
instance Transformable2 a => Transformable2Z a
Note that the operator (%$) appears in both Transformable2 and Transformable2Z. Usually this causes a name clash, but not here since the static version is in the GeometryStatic module and imported qualified.
-- 3D Vectors
xVector3 :: Zippable z => z Vector3 -- unit vector
yVector3 :: Zippable z => z Vector3 -- unit vector
zVector3 :: Zippable z => z Vector3 -- unit vector
vector3XYZ :: Zippable z => z FRPReal -> z FRPReal ->
z FRPReal -> z Vector3
Vector3XYZ :: pattern constructor -- static version
only
vector3Spherical :: Zippable z => z FRPReal -> z Angle ->
z Angle -> z Vector3
vector3XYZCoords :: Zippable z => z Vector3 ->
z (FRPReal, FRPReal, FRPReal)
vector3XYZCoordsTuple :: Zippable z => z Vector3 ->
(z FRPReal, z FRPReal, z FRPReal)
-- lifted version
only
vector3XCoord :: Zippable z => z Vector3 -> z FRPReal
vector3YCoord :: Zippable z => z Vector3 -> z FRPReal
vector3ZCoord :: Zippable z => z Vector3 -> z FRPReal
vector3SphericalCoords :: Zippable z => z Vector3 ->
z (FRPReal, Angle, Angle)
vector3SphericalCoordsTuple :: Zippable z => z Vector3 ->
(z FRPReal, z Angle, z Angle)
-- lifted version
only
instance Num Vector3 -- fromInteger, * not allowed
instance Zippable z => Num (z Vector3) -- fromInteger, * not allowed
-- 3D Points
origin3 :: Zippable z => z Point3
point3XYZ :: Zippable z => z FRPReal -> z FRPReal ->
z FRPReal -> z Point3
Point3XYZ :: pattern constructor -- static version only
point3Spherical :: Zippable z => z FRPReal -> z Angle -> z Angle
->
z Point3
point3XYZCoords :: Zippable z => z Point3 ->
z (FRPReal, FRPReal, FRPReal)
point3XYZCoordsTuple :: Zippable z => z Point3 ->
(z FRPReal, z FRPReal, z FRPReal)
-- lifted version
only
point3XCoord :: Zippable z => z Point3 -> z FRPReal
point3YCoord :: Zippable z => z Point3 -> z FRPReal
point3ZCoord :: Zippable z => z Point3 -> z FRPReal
point3SphericalCoords :: Zippable z => z Point3 ->
z (FRPReal, Angle, Angle)
point3SphericalCoordsTuple:: Zippable z => z Point3 ->
(z FRPReal, z Angle, z Angle)
-- lifted version
only
distance3 :: Zippable z => z Point3 -> z Point3 -> z FRPReal
distance3Squared :: Zippable z => z Point3 -> z Point3 -> z FRPReal
linearInterpolate3 :: Zippable z => z Point3 -> z Point3 -> z FRPReal
->
z Point3
point3ToVector3 :: Zippable z => z Point3 -> z Vector3
vector3ToPoint3 :: Zippable z => z Vector3 -> z Point3
mirrorX3 :: Zippable z => z Point3 -> z Point3
mirrorY3 :: Zippable z => z Point3 -> z Point3
mirrorZ3 :: Zippable z => z Point3 -> z Point3
mirrorXY3 :: Zippable z => z Point3 -> z Point3
mirrorXZ3 :: Zippable z => z Point3 -> z Point3
mirrorYZ3 :: Zippable z => z Point3 -> z Point3
mirrorXYZ3 :: Zippable z => z Point3 -> z Point3
(.+^#), (.-^#) :: Zippable z => z Point3 -> z Vector3 -> z Point3
(.-.#) :: Zippable z => z Point3 -> z Point3 -> z Vector3
-- 3D Vector Spaces
instance VectorSpace Vector3
Please refer to section 6.1.1 for the definition of the VectorSpace class.
-- 3D Transformations
identity3 :: Zippable z => z Transform3
translate3 :: Zippable z => z Vector3 -> z Transform3
rotate3 :: Zippable z => z Vector3 -> z FRPReal -> z Transform3
scale3 :: Zippable z => z Vector3 -> z Transform3
compose3 :: Zippable z => z Transform3 -> z Transform3 -> z Transform3
uscale3 :: Zippable z => z FRPReal -> z Transform3
class Transformable3 a where
(%$#) :: Transform3 -> a -> a -- not currently used
class Tranformable3Z a where
(%$#) :: Zippable z => z Transform3 -> z a -> z a -- not currently used
instance Transformable3 a => Transformable3Z a
The standard distribution of FRP includes a graphics module, which can
be very useful for animation and debugging. To use graphics, import
GraphicsFRP instead of FRP. The graphics operations are
lifted. To access the static version, you can import the
Graphics module qualified. The following example shows how:
module Follow where
import GraphicsFRP
import qualified GeometryStatic as S
import qualified Graphics as G
p1, p2 :: Behavior UserAction Point2
p1 = (point2XY 2 2) .+^ integralB (0.7 *^ (mouseB .-. p1))
p2 = origin2 .+^ integralB (0.5 *^ (p1 .-. p2))
world = pairZ p1 p2
showWorld (p1,p2) =
S.moveTo p1 (G.withColor Red obj) `G.over`
S.moveTo p2 (G.withColor Blue obj)
where obj = S.stretch 0.3 G.circle
main = animate $ lift1 showWorld world
In this example, a red ball (p1) follows the mouse movement, and a blue ball (p2) follows the red ball at a slower speed. This example also shows how to work with points and vectors.
The graphics library contains the following drawing primitives:
data Color = Black | Blue | Green | Cyan | Red | Magenta | Yellow | White
data Picture = ...
instance Transformable2 Picture
black, blue, green, cyan, red, magenta, yellow, white
:: Zippable z => z Color
over :: Zippable z => z Picture -> z Picture -> z Picture
emptyPic :: Zippable z => z Picture
line :: Zippable z => z Point2 -> z Point2 -> z Picture
polygon :: Zippable z => [z Point2] -> z Picture
polyline :: Zippable z => [z Point2] -> z Picture
text :: Zippable z => z String -> z Picture
arc :: Zippable z => z Point2 -> z Point2 -> z Angle -> z Angle
-> z Picture
circle :: Zippable z => z Picture
withColor :: Zippable z => z Color -> z Picture -> z Picture
rect :: Zippable z => z Point2 -> z Point2 -> z Picture
rectline :: Zippable z => z Point2 -> z Point2 -> z Picture
square :: Zippable z => z Picture
squareline :: Zippable z => z Picture
The static version of these primitives can be obtained by importing Graphics qualified, as already shown in the Follow example.
To get user inputs, use the following primitives:
data UserAction = ... GUI user actions
keyE :: Event UserAction Char -- keyboard inputs
mmE :: Event UserAction Point2 -- mouse moves
mouseB :: Behavior UserAction Point2 -- current mouse position
lbpE :: Event UserAction () -- left mouse button presses
rbpE :: Event UserAction () -- right mouse button presses