Functional Reactive Programming with
reactive-banana and vty
I’ve recently been experimenting with Haskell’s reactive-banana,
a functional reactive programming (FRP) library. I couldn’t get the
reactive-banana
examples to work because of issues with wxWidgets,
so I used vty
instead. vty has fewer dependencies, and will provide
a good demonstration of how to connect reactive-banana
to a GUI library yourself.
A Quick Introduction to
vty
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
little about vty.
The following code is a complete vty program which
will print 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 ()
main = do
vty <- V.mkVty V.defaultConfig
_ <- forkIO $ forever $ loop vty
threadDelay $ 10 * 1000000
V.shutdown vty
loop :: V.Vty -> IO ()
loop vty = do
e <- V.nextEvent vty
showEvent e
where
showEvent :: V.Event -> IO ()
showEvent = V.update vty . V.picForImage . V.string V.defAttr . showA few notes about the code:
-
The
mainfunction initializesvtyand starts theloopfunction in a separate thread, and then ends the entire program after 10 seconds. In Haskell all threads end when the main thread ends. -
The
loopfunction receives anV.Eventfromvtyand then prints it over and over in a loop. TheV.nextEventfunction blocks the thread until the nextV.Eventfires. In this case, allV.Events are caused by keypresses. -
The
showEventfunction is a bit complicated due tovty’s API. Basically,V.updaterenderes “images” to the screen, and images are made up of “pictures”, andV.String V.defAttris a partially applied function that makes pictures. Note thatV.defAttris short for “default attr” (rather than “define attr”) and causesV.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 withreactive-banana.
For the specifics check out the vty API
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.
Connecting
vty to reactive-banana
Before we connect reactive-banana to
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; reactive-banana
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 ()
function.
In reactive-banana we will built a “network” out of
“Events” and “Behaviors” and about ten
primative combinators. The conceptual network formed by these
Events, 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 reactive-banana without
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
code much.
import Control.Concurrent (threadDelay, forkIO)
import Control.Monad (forever)
import qualified Graphics.Vty as V
import Reactive.Banana
import Reactive.Banana.Frameworks
main :: IO ()
main = do
vty <- V.mkVty V.defaultConfig
reactiveBananaNetwork <- network vty
actuate reactiveBananaNetwork
threadDelay $ 10 * 1000000
V.shutdown vty
network :: V.Vty -> IO EventNetwork
network vty = compile $ do
(inputE, fireInputE) <- newEvent
_ <- liftIO $ forkIO $ forever $ fireInputE =<< V.nextEvent vty
reactimate $ fmap showEvent inputE
where
showEvent :: V.Event -> IO ()
showEvent = V.update vty . V.picForImage . V.string V.defAttr . showAbout the code:
-
reactive-bananaandvtyboth have a type calledEvent. Do not get them mixed up. AvtyEventis always something like “a key was pressed”, while areactive-bananaEventis more general in meaning like “something happened” where you’re able to choose what that “something” is. The type ofinputEisEvent V.Event, which means it’s areactive-bananaEventcontaining avtyEvent; 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. -
In the
mainfunction we create areactiveBananaNetworkwhich has the typeEventNetwork. Inreactive-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 anEventNetwork. -
In the main function
actuateruns theEventNetwork, with all its inputs and outputs, in a separate thread. -
In the network function
newEventcreates areactive-bananaEventnamedinputE.EventsandBehaviorsare the two basic types that make up an FRP network. We will talk more about them later. ThefireInputEshould be called from outside the FRP network, and will cause theinputEinside the network to fire. -
fireInputE =<< V.nextEvent vtyruns in an infinite loop. It repeatedly callsV.nextEventwhich blocks until avtyEventoccurs, and when anEventoccurs it fires thatEventinto the FRP network. This is necessary becausereactive-bananadoesn’t have a way to poll a blocking function likeV.nextEvent, but we can make our own adapter with this one line. -
reactimateis how the FRP network runs output actions.
Building Logic with
reactive-banana
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 ()
main = do
vty <- V.mkVty V.defaultConfig
actuate =<< network vty
threadDelay $ 20 * 1000000
V.shutdown vty
update :: V.Vty -> [String] -> IO ()
update vty = V.update vty . V.picForImage . mconcat . fmap (V.string V.defAttr)
network :: V.Vty -> IO EventNetwork
network vty = compile $ do
(inputEvents, fireInputEvent) <- newEvent
_ <- liftIO $ forkIO $ forever $ fireInputEvent =<< V.nextEvent vty
outputEvents <- liftMoment $ pureNetwork inputEvents
reactimate' =<< changes (fmap (update vty) outputEvents)
isChar :: V.Event -> Bool
isChar (V.EvKey (V.KChar _) _) = True
isChar _ = False
isEsc :: V.Event -> Bool
isEsc (V.EvKey (V.KEsc) _) = True
isEsc _ = False
mightCapitalize :: Bool -> String -> String
mightCapitalize True = fmap toUpper
mightCapitalize False = id
pureNetwork :: Event V.Event -> Moment (Behavior [String])
pureNetwork inputEvents = do
let inputChar = (\(V.EvKey (V.KChar c) _) -> pure c) <$>
filterE isChar inputEvents
let inputEsc = filterE isEsc inputEvents
accumedString <- accumB "" ((\new accumed -> accumed ++ new) <$> inputChar)
capitalizeSwitch <- accumE False (not <$ inputEsc)
maybeCapitalize <- switchB (pure $ mightCapitalize False)
(pure . mightCapitalize <$> capitalizeSwitch)
pure $ fmap pure $ maybeCapitalize <*> accumedStringThe core logic and state is managed in
pureNetwork.
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 documentation.
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
hint, see
fromPoll.