Functional Reactive Programming with
I’ve recently been experimenting with Haskell’s
a functional reactive programming (FRP) library. I couldn’t get the
examples to work because of issues with
so I used
vty has fewer dependencies, and will provide
a good demonstration of how to connect
to a GUI library yourself.
A Quick Introduction to
vty is a
terminal GUI library similar to
ncurses. By using a
simple terminal GUI we will be able to spend more time focused on
reactive-banana itself, but first we need to learn a
The following code is a complete
vty program which
Events as keys are pressed. The program
ends after 10 seconds, and will not respond to Ctrl-C or
other signals, so just wait it out. While the program
runs, experiment by pressing a variety of keys.
import Control.Concurrent (threadDelay, forkIO) import Control.Monad (forever) import qualified Graphics.Vty as V main :: IO () = do main <- V.mkVty V.defaultConfig vty <- forkIO $ forever $ loop vty _ $ 10 * 1000000 threadDelay V.shutdown vty loop :: V.Vty -> IO () = do loop vty <- V.nextEvent vty e showEvent ewhere showEvent :: V.Event -> IO () = V.update vty . V.picForImage . V.string V.defAttr . show showEvent
A few notes about the code:
vtyand starts the
loopfunction in a separate thread, and then ends the entire program after 10 seconds. In Haskell all threads end when the main thread ends.
loopfunction receives an
vtyand then prints it over and over in a loop. The
V.nextEventfunction blocks the thread until the next
V.Eventfires. In this case, all
V.Events are caused by keypresses.
showEventfunction is a bit complicated due to
vty’s API. Basically,
V.updaterenderes “images” to the screen, and images are made up of “pictures”, and
V.String V.defAttris a partially applied function that makes pictures. Note that
V.defAttris short for “default attr” (rather than “define attr”) and causes
V.stringto use the default terminal colors.
It’s not necissary to use multiple threads with
vty, and it’s probably best to use a single thread in most cases. In this case though, that second thread will end up being useful when we integrate with
For the specifics check out the
documentation on Hackage.
With just a few lines of code we have a real-time interactive program. Now we can experiment with functional reactive programming.
Before we connect
vty we need a high-level understanding of how
reactive-banana works. What exactly does
reactive-banana do? This might sound a little
underwhelming, but bear with me;
allows you to express IO actions in terms of input events. It’s
simply an alternative way of implementing an
Events -> IO
() function! Except,
Events -> IO () isn’t
always simple; who know’s what state is hiding in there or how that
state is structured? It can be anything. Functional reactive
programming gives us a standard way to express the logic and manage
the state inside a conceptual
Events -> IO ()
reactive-banana we will built a “network” out of
Events” and “
Behaviors” and about ten
primative combinators. The conceptual network formed by these
Behaviors, and combinators is
called an “event graph”, although there is no type by this name. An
“event graph” can be connected to the real world in a variety of
ways, but the implementation will stay the same, this allows us to
build abstract components in
worrying about how they will be connected to the real world. When
the “event graph” is ultimately connected to real world inputs and
outputs it becomes an “
EventNetwork”. Below we will
build an extremely simple
EventNetwork and use it in
our program. There is no benefit to such a simple
EventNetwork, but it will allow us to see how an
EventNetwork is connected to the rest of the program.
We will later build more sophisticated behaviors inside the
EventNetwork without needing to alter the surrounding
import Control.Concurrent (threadDelay, forkIO) import Control.Monad (forever) import qualified Graphics.Vty as V import Reactive.Banana import Reactive.Banana.Frameworks main :: IO () = do main <- V.mkVty V.defaultConfig vty <- network vty reactiveBananaNetwork actuate reactiveBananaNetwork$ 10 * 1000000 threadDelay V.shutdown vty network :: V.Vty -> IO EventNetwork = compile $ do network vty <- newEvent (inputE, fireInputE) <- liftIO $ forkIO $ forever $ fireInputE =<< V.nextEvent vty _ $ fmap showEvent inputE reactimate where showEvent :: V.Event -> IO () = V.update vty . V.picForImage . V.string V.defAttr . show showEvent
About the code:
vtyboth have a type called
Event. Do not get them mixed up. A
Eventis always something like “a key was pressed”, while a
Eventis more general in meaning like “something happened” where you’re able to choose what that “something” is. The type of
Event V.Event, which means it’s a
Event; we might think of it as “something happened, and that something was a key being pressed”. Remember that “Event” is an overloaded term in these examples.
mainfunction we create a
reactiveBananaNetworkwhich has the type
reactive-bananathere are conceptual “event graphs” which contain logic and state and are isolated from the real world; when these “event graphs” are combined with the real world by connecting inputs and outputs they become an
In the main function
EventNetwork, with all its inputs and outputs, in a separate thread.
In the network function
Behaviorsare the two basic types that make up an FRP network. We will talk more about them later. The
fireInputEshould be called from outside the FRP network, and will cause the
inputEinside the network to fire.
fireInputE =<< V.nextEvent vtyruns in an infinite loop. It repeatedly calls
V.nextEventwhich blocks until a
Eventoccurs, and when an
Eventoccurs it fires that
Eventinto the FRP network. This is necessary because
reactive-bananadoesn’t have a way to poll a blocking function like
V.nextEvent, but we can make our own adapter with this one line.
reactimateis how the FRP network runs output actions.
Building Logic with
Finally, I’ll end with a more complex example. This program allows you to type in your terminal, and pressing escape will toggle capitalization of the letters.
import Control.Concurrent (threadDelay, forkIO) import Control.Monad (forever) import Data.Char (toUpper) import qualified Graphics.Vty as V import Reactive.Banana import Reactive.Banana.Frameworks main :: IO () = do main <- V.mkVty V.defaultConfig vty =<< network vty actuate $ 20 * 1000000 threadDelay V.shutdown vty update :: V.Vty -> [String] -> IO () = V.update vty . V.picForImage . mconcat . fmap (V.string V.defAttr) update vty network :: V.Vty -> IO EventNetwork = compile $ do network vty <- newEvent (inputEvents, fireInputEvent) <- liftIO $ forkIO $ forever $ fireInputEvent =<< V.nextEvent vty _ <- liftMoment $ pureNetwork inputEvents outputEvents =<< changes (fmap (update vty) outputEvents) reactimate' isChar :: V.Event -> Bool V.EvKey (V.KChar _) _) = True isChar (= False isChar _ isEsc :: V.Event -> Bool V.EvKey (V.KEsc) _) = True isEsc (= False isEsc _ mightCapitalize :: Bool -> String -> String True = fmap toUpper mightCapitalize False = id mightCapitalize pureNetwork :: Event V.Event -> Moment (Behavior [String]) = do pureNetwork inputEvents let inputChar = (\(V.EvKey (V.KChar c) _) -> pure c) <$> filterE isChar inputEventslet inputEsc = filterE isEsc inputEvents <- accumB "" ((\new accumed -> accumed ++ new) <$> inputChar) accumedString <- accumE False (not <$ inputEsc) capitalizeSwitch <- switchB (pure $ mightCapitalize False) maybeCapitalize pure . mightCapitalize <$> capitalizeSwitch) (pure $ fmap pure $ maybeCapitalize <*> accumedString
The core logic and state is managed in
It’s been over 2 years since I started writing this post. I lost
momentum, and then lost interest in publishing a blog until
recently. I’ve lost most of the insights I had to offer, so I will
instead refer you to the
reactive-banana is relatively small compared to other
FRP frameworks, and has good documentation.
Hopefully this small self-contained example will give you a
starting point for your own experiments. As an exercise, modify the
program so that toggling capitalization is throttled to once every
4 seconds, but other keypresses should remain unthrottled. As a