reactive-banana is a mature library very similar to reactive. We won't try to reinvent an frp library; instead we'll explore how to integrate reactive-banana into a project for ourselves.
The big picture
To use a functional reactive programming library like reactive-banana with OpenGL we will divide the work into 4 parts, 2 of which already exist. We will use the existing GLUT library to interact with OpenGL, and the existing reactive-banana library for an implementation of functional reactive programming. We will provide 2 parts of our own. The first part we will provide is a framework that will connect GLUT to reactive-banana. The second part we will provide is the program that will be written in terms of the frp implementation (reactive-banana) and framework and GLUT types.
Both of the parts that we provide will be written in terms of the reactive-banana frp library. The library has two big ideas, Event t a
and Behavior t a
. Event t a
represents events carrying data of type a
that occur at different points in time. Behavior t a
represents a time varying value of type a
that is defined at all points in time. The t
type argument we are required by the type system to preserve but otherwise ignore.
Most of the interface to Event
and Behavior
are hidden in their instances. Event
is a Functor
- we can fmap
or <$>
a function over the values of any Event
.
fmap :: (a -> b) -> Event t a -> Event t b
Behavior
is both Applicative
and a Functor
. We can fmap
or <$>
a function over all the values a Behavior
takes on, can provide new constant unchanging values with pure
, and calculate new Behavior
s with <*>
.
fmap :: (a -> b) -> Behavior t a -> Behavior t b
pure :: a -> Behavior t a
<*> :: Behavior t (a -> b) -> Behavior t a -> Behavior t b
There are a few other functions provided by reactive-banana that provide functionality that can't be represented in terms of base typeclasses. These introduce statefulness, combine Event
s together, and convert between Event
s and Behavior
s.
State is introduced by accumE
which takes an initial value and an Event
of changes from the previous value to a new value and produces an Event
of the new values. accumB
produces a Behavior
instead
accumE :: a -> Event t (a -> a) -> Event t a
accumB :: a -> Event t (a -> a) -> Behavior t a
union
combines two event streams together
union :: Event t a -> Event t a -> Event t a
stepper
can convert an Event
to a Behavior
holding the most recent value if we provide an initial value so that it is defined at all points in time. apply
or <@>
can convert a Behavior
into an Event
if we provide a series of Events
at which to poll the current value of the Behavior
.
stepper :: a -> Event t a -> Behavior t a
<@> :: Behavior t (a -> b) -> Event t a -> Event t b
The instances for Event
and Behavior
and the 19 functions in Reactive.Banana.Combinators make up the entire interface for functional reactive programming.
Overall, we will need the GLUT library and libraries used by the OpenGL example we are implementing, the reactive-banana library, the reactive-banana exports for making frameworks and the RankNTypes extension, a couple mechanisms for interthread communication, and the ability to read the system clock.
{-# LANGUAGE RankNTypes #-}
import Graphics.UI.GLUT
import Control.Monad
import Reactive.Banana
import Reactive.Banana.Frameworks
import Data.IORef
import Control.Concurrent.MVar
import Data.Time
The framework interface
Our framework will map the IO
events from GLUT to reactive-banana Event
s and Behavior
s. There are four GLUT events that the example uses - reshapeCallback
, keyboardMouseCallback
, idleCallback
, and displayCallback
. We will map these to Event
s and Behavior
s.
reshapeCallback
is run when the user resizes the window. As a callback, it required something of the type type ReshapeCallback = Size -> IO ()
. We will represent this as an Event t Size
.
keyboardMouseCallback
is run when the user provides keyboard input, moves the mouse, or clicks a mouse button. As a callback, it required something of the type type KeyboardMouseCallback = Key -> KeyState -> Modifiers -> Position -> IO ()
. We will represent this as an input with type Event t KeyboardMouse
, where KeyboardMouse
bundles together all of the arguments passed to the callback.
data KeyboardMouse = KeyboardMouse {
key :: Key,
keyState :: KeyState,
modifiers :: Modifiers,
pos :: Position
}
idleCallback
is run when time passes. We will represent this as a behavior that tracks the amount of time that has passed, Behavior t DiffTime
. Because it is a Behavior
instead of an Event
, our program won't be able to directly observe time passing. If this isn't desired, we could use an Event
instead.
Bundling all of the inputs together we get
data Inputs t = Inputs {
keyboardMouse :: Event t KeyboardMouse,
time :: Behavior t DiffTime,
reshape :: Event t Size
}
displayCallback
is different from the other callbacks; it isn't for the input to the program, but instead is for outputting what needs to be displayed. Since GLUT could run this at any time to try to display something on the screen, it makes sense for it to be defined at all points in time. We will represent this output with a Behavior t DisplayCallback
.
There is one more output we will need - in response to events the example program occasionally produces other IO actions. We will allow the program to raise events to execute arbitrary IO with an Event t (IO ())
.
Bundling both outputs together we get
data Outputs t = Outputs {
display :: Behavior t DisplayCallback,
whenIdle :: Event t (IO ())
}
Our framework will be invoked by passing it a program with the type forall t. Inputs t -> Outputs t
. We will define program
and reactiveGLUT
in the next two sections.
main :: IO ()
main = do
(progname,_) <- getArgsAndInitialize
initialDisplayMode $= [DoubleBuffered]
createWindow "Hello World"
reactiveGLUT program
The program
The program will use reactive-banana to map the Inputs
to the Outputs
. To get started porting the tutorial code, we'll remove the IORef
s from cubes
and rename reshape
to onReshape
since it conflicts with a name from our framework interface.
cubes :: GLfloat -> (GLfloat, GLfloat) -> DisplayCallback
cubes a (x',y') = do
clear [ColorBuffer]
loadIdentity
translate $ Vector3 x' y' 0
preservingMatrix $ do
rotate a $ Vector3 0 0 1
scale 0.7 0.7 (0.7::GLfloat)
forM_ (points 7) $ \(x,y,z) -> preservingMatrix $ do
color $ Color3 ((x+1)/2) ((y+1)/2) ((z+1)/2)
translate $ Vector3 x y z
cube 0.1
swapBuffers
onReshape :: ReshapeCallback
onReshape size = do
viewport $= (Position 0 0, size)
keyboardMouse
will be completely replaced by positionChange
and angleSpeedChange
. These convert a KeyboardMouse
event into a change to make to either the position or the speed the cubes are rotating. When no change is needed for an event, they return Nothing
.
positionChange :: Fractional a => KeyboardMouse -> Maybe ((a, a) -> (a, a))
positionChange (KeyboardMouse (SpecialKey k) Down _ _) = case k of
KeyLeft -> Just $ \(x,y) -> (x-0.1,y)
KeyRight -> Just $ \(x,y) -> (x+0.1,y)
KeyUp -> Just $ \(x,y) -> (x,y+0.1)
KeyDown -> Just $ \(x,y) -> (x,y-0.1)
_ -> Nothing
positionChange _ = Nothing
angleSpeedChange :: Num a => KeyboardMouse -> Maybe (a -> a)
angleSpeedChange (KeyboardMouse (Char c) Down _ _) = case c of
' ' -> Just negate
'+' -> Just (+1)
'-' -> Just (subtract 1)
_ -> Nothing
angleSpeedChange _ = Nothing
Calculating the position is fairly easy, we accumulate the changes from the keyboard inputs. filterJust :: Event t (Maybe a) -> Event t a
throws out the events that we weren't interested in.
positionB :: Fractional a => Inputs t -> Behavior t (a, a)
positionB = accumB (0.0, 0.0) . filterJust . fmap positionChange . keyboardMouse
We'll calculate the angle of the rotating cubes a bit differently. We'll remember the time and angle when the speed changes, apply a function that calculates the difference in angle to the difference in times, and add that to the initial angle.
angleCalculation :: (Num a, Num b) => a -> b -> (a -> b) -> a -> b
angleCalculation a0 b0 f a1 = f (a1 - a0) + b0
Calculating the angle
is a bit more difficult. First we compute an event, angleF :: Event t (DiffTime -> GLfloat)
, holding a function from a difference between times to a difference between angles. We lift and apply our angleCalculation
to the current time
and angle
, and poll that at each occurrence of the angleF
event. We convert the polled function into a Behavior
with stepper
and apply it to the current time
.
angleB :: Fractional a => Inputs t -> Behavior t a
angleB inputs = angle
where
initialSpeed = 2
angleSpeed = accumE initialSpeed . filterJust . fmap angleSpeedChange . keyboardMouse $ inputs
scaleSpeed x y = 10 * x * realToFrac y
angleF = scaleSpeed <$> angleSpeed
angleSteps = (angleCalculation <$> time inputs <*> angle) <@> angleF
angle = stepper (scaleSpeed initialSpeed) angleSteps <*> time inputs
The whole program
maps Inputs
to Outputs
. It says that the behavior for what to display
is cubes
lifted and applied to the angle and position. The Event
for other IO
side effects is onReshape
every time the reshape
event happens.
program :: Inputs t -> Outputs t
program inputs = outputs
where
outputs = Outputs {
display = cubes <$> angleB inputs <*> positionB inputs,
whenIdle = onReshape <$> reshape inputs
}
The framework
Our framework accepts a program with the type forall t. Inputs t -> Outputs t
and runs it. To implement the framework, we use the functions in Reactive.Banana.Frameworks
. These functions allow us to raise Event
s from IO
and run IO
actions in response to Event
s. We can make Behavior
s from Event
s and poll Behavior
s when Event
s occur using the functions from Reactive.Banana.Combinators
.
reactiveGLUT :: (forall t. Inputs t -> Outputs t) -> IO ()
reactiveGLUT program = do
-- Initial values
initialTime <- getCurrentTime
-- Events
(addKeyboardMouse, raiseKeyboardMouse) <- newAddHandler
(addTime, raiseTime) <- newAddHandler
(addReshape, raiseReshape) <- newAddHandler
(addDisplay, raiseDisplay) <- newAddHandler
newAddHandler
creates a handle with which to talk about an Event t a
, and a function to raise the event of type a -> IO ()
. We make the obvious events for keyboard and mouse input, idle time passing, and the window shape changing. We also make an event that we will use to poll the display
Behavior
when we need to run it in the displayCallback
.
We have one tricky problem to overcome - OpenGL requires all the UI interaction to happen in a specific thread, but we aren't sure what thread the actions we bind to reactive-banana events will happen in. We'll use a couple of variables shared across threads to make sure the Output
IO
is run in the OpenGL thread. For display
output, we'll use an MVar
to store the polled display
action. For IO
actions that are queued in whenIdle
we'll accumulate them in an IORef
,
-- output variables and how to write to them
displayVar <- newEmptyMVar
whenIdleRef <- newIORef (return ())
let
setDisplay = putMVar displayVar
runDisplay = takeMVar displayVar >>= id
addWhenIdle y = atomicModifyIORef' whenIdleRef (\x -> (x >> y, ()))
runWhenIdle = atomicModifyIORef' whenIdleRef (\x -> (return (), x)) >>= id
Our whole network consists of the following parts. First we create Event
s (using fromAddHandler
) or Behavior
s (using fromChanges
) for each of the Inputs
and an Event
for polling the output display
. We perform a small amount of processing to simplify the clock. We apply the program
to the inputs
we prepared to get the program's Outputs
. Using <@
, we poll the display
whenever our display event happens. Finally, reactimate
tells reactive-banana to run setDisplay
or addWhenIdle
whenever the corresponsonding Event
occurs. Once we have described the network we compile
and actuate
it.
-- Reactive network for GLUT programs
let networkDescription :: forall t. Frameworks t => Moment t ()
networkDescription = do
keyboardMouseEvent <- fromAddHandler addKeyboardMouse
clock <- fromChanges initialTime addTime
reshapeEvent <- fromAddHandler addReshape
displayEvent <- fromAddHandler addDisplay
let
diffTime = realToFrac . (flip diffUTCTime) initialTime <$> clock
inputs = Inputs keyboardMouseEvent diffTime reshapeEvent
outputs = program inputs
displayPoll = display outputs <@ displayEvent
reactimate $ fmap setDisplay displayPoll
reactimate $ fmap addWhenIdle (whenIdle outputs)
network <- compile networkDescription
actuate network
For each of the GLUT callbacks we are interested in we raise the corresponding reactive-banana Event
. For the idle callback we also run any queued events. For the display callback, we run the polled DisplayCallback
.
-- Handle GLUT events
keyboardMouseCallback $= Just (\k ks m p -> raiseKeyboardMouse (KeyboardMouse k ks m p))
idleCallback $= Just (do
getCurrentTime >>= raiseTime
runWhenIdle
postRedisplay Nothing)
reshapeCallback $= Just raiseReshape
displayCallback $= do
raiseDisplay ()
runDisplay
mainLoop
The rest of the example
The rest of the tutorial code can be repeated verbatim
vertex3f :: (GLfloat, GLfloat, GLfloat) -> IO ()
vertex3f (x, y, z) = vertex $ Vertex3 x y z
points :: Int -> [(GLfloat,GLfloat,GLfloat)]
points n = [ (sin (2*pi*k/n'), cos (2*pi*k/n'), 0) | k <- [1..n'] ]
where n' = fromIntegral n
cube :: GLfloat -> IO ()
cube w = renderPrimitive Quads $ mapM_ vertex3f
[ ( w, w, w), ( w, w,-w), ( w,-w,-w), ( w,-w, w),
( w, w, w), ( w, w,-w), (-w, w,-w), (-w, w, w),
( w, w, w), ( w,-w, w), (-w,-w, w), (-w, w, w),
(-w, w, w), (-w, w,-w), (-w,-w,-w), (-w,-w, w),
( w,-w, w), ( w,-w,-w), (-w,-w,-w), (-w,-w, w),
( w, w,-w), ( w,-w,-w), (-w,-w,-w), (-w, w,-w) ]