I am learning reactive-banana. In order to understand the library I have decide to implement a dummy application that would increase a counter whenever someone pushes a button.
The UI library I am using is Gtk but that is not relevant for the explanation.
Here is the very simple implementation that I have come up with:
import Graphics.UI.Gtk
import Reactive.Banana
import Reactive.Banana.Frameworks
makeNetworkDescription addEvent = do
eClick <- fromAddHandler addEvent
reactimate $ (putStrLn . show) <$> (accumE 0 ((+1) <$ eClick))
main :: IO ()
main = do
(addHandler, fireEvent) <- newAddHandler
initGUI
network <- compile $ makeNetworkDescription addHandler
actuate network
window <- windowNew
button <- buttonNew
set window [ containerBorderWidth := 10, containerChild := button ]
set button [ buttonLabel := "Add One" ]
onClicked button $ fireEvent ()
onDestroy window mainQuit
widgetShowAll window
mainGUI
This just dumps the result in the shell. I came up to this solution reading the article by Heinrich Apfelmus. Notice that in my example I have not used a single Behavior
.
In the article there is an example of a network:
makeNetworkDescription addKeyEvent = do
eKey <- fromAddHandler addKeyEvent
let
eOctaveChange = filterMapJust getOctaveChange eKey
bOctave = accumB 3 (changeOctave <$> eOctaveChange)
ePitch = filterMapJust (`lookup` charPitches) eKey
bPitch = stepper PC ePitch
bNote = Note <$> bOctave <*> bPitch
eNoteChanged <- changes bNote
reactimate' $ fmap (\n -> putStrLn ("Now playing " ++ show n))
<$> eNoteChanged
The example show a stepper
that transforms an Event
into a Behavior
and brings back an Event
using changes
. In the above example we could have used only Event
and I guess that it would have made no difference (unless I am not understanding something).
So could someone can shed some light on when to use Behavior
and why? Should we convert all Event
s as soon as possible?
In my little experiment I don't see where Behavior
can be used.
Thanks
Behavior
s have a value all the time, whereasEvent
s only have a value at an instant.Think of it like you would in a spreadsheet - most of the data exists as stable values (Behaviors) that hang around and get updated whenever necessary. (In FRP though, the dependency can go either way without circular reference problems - the data is updated flowing from the changed value to unchanged ones.) You can additionally add code that fires when you press a button or do something else, but most of the data is available all the time.
Certainly you could do all that with just events - when this changes, read this value and that value and output this value, but it's just cleaner to express those relationships declaratively and let the spreadsheet or compiler worry about when to update stuff for you.
stepper
is for changing things that happen into values in cells, andchange
is for watching cells and triggering actions. Your example where the output is text on a command line isn't particularly affected by the lack of persistent data, because the output comes in bursts anyway.If however you have a graphical user interface, the event-only model, whilst certainly possible, and indeed common, is a little cumbersome compared to the FRP model. In FRP you just specify the relationships between things without being explicit about updates.
It's not necessary to have Behaviors, and analogously you could program an Excel spreadsheet entirely in VBA with no formulae. It's just nicer with the data permanence and equational specification. Once you're used to the new paradigm, you'll not want to go back to manually chasing dependencies and updating stuff.
Anytime the FRP network "does something" in Reactive Banana it's because it's reacting to some input event. And the only way it does anything observable outside the system is by wiring up an external system to react to events it generates (using
reactimate
).So if all you're doing is immediately reacting to an input event by producing an output event, then no, you won't find much reason to use
Behaviour
.Behaviour
is very useful for producing program behaviour that depends on multiple event streams, where you have to remember that events happen at different times.An
Event
has occurrences; specific instants of time where it has a value. ABehaviour
has a value at all points in time, with no instants of time that are special (except withchanges
, which is convenient but kind of model-breaking).A simple example familiar from many GUIs would be if I want to react to mouse clicks and have shift-click do something different from a click when the shift key is not held. With a
Behaviour
holding a value indicating whether the shift key is held down, this is trivial. If I just hadEvent
s for shift key press/release and for mouse clicks it's much harder.In addition to being harder, it's much more low level. Why should I have to do complicated fiddling just to implement a simple concept like shift-click? The choice between
Behaviour
andEvent
is a helpful abstraction for implementing your program's concepts in terms that map more closely to the way you think about them outside the programming world.An example here would be a movable object in a game world. I could have an
Event Position
representing all the times it moves. Or I could just have aBehaviour Position
representing where it is at all times. Usually I'll be thinking of the object as having a position at all times, soBehaviour
is a better conceptual fit.Another place
Behaviour
s are useful is for representing external observations your program can make, where you can only check the "current" value (because the external system won't notify you when changes occur).For an example, let's say your program has to keep tabs on a temperature sensor and avoid starting a job when the temperature is too high. With an
Event Temperature
I'll have decide up front how often to poll the temperature sensor (or in response to what). And then have all the same issues as in my other examples about having to manually do something to make the last temperature reading available to the event that decides whether or not to start a job. Or I could usefromPoll
to make aBehaviour Temperature
. Now I've got a value that represents the time-varying value of the temperature, and I've completely abstracted away from polling the sensor; Reactive Banana itself takes care of polling the sensor as often as it might be needed without me needing to impending any logic for that at all!When you have only 1 Event, or multiple Events that happen simultaneously, or multiple Events of the same type, it's easy to just
union
or otherwise combine them into a resulting Event, then pass toreactimate
and immediately output it. But what if you have 2 Events of 2 different types happening at different times? Then combining them into a resulting Event that you can pass toreactimate
becomes an unnecessary complication.I recommend you to actually try and implement the synthesizer from FRP explanation using reactive-banana with only Events and no Behaviors, you'll quickly see that Behaviors simplify the unnecessary Event manipulations.
Say we have 2 Events, outputting Octave (type synonym for Int) and Pitch (type synonym to Char). User presses keys from a to g to set current pitch, or presses + or - to increment or decrement current octave. The program should output current pitch and current octave, like
a0
,b2
, orf7
. Let's say the user pressed these keys in various combinations during different times, so we ended up with 2 event streams (Events) like that:Every time user presses a key, we output current octave and pitch. But what should be the result event? Suppose default pitch is
a
and default octave is0
. We should end up with an event stream that looks like this:Simple character input/output
Let's try to implement the synthesizer from scratch and see if we can do it without Behaviors. Let's first write a program, where you put a character, press Enter, the program outputs it, and asks for a character again:
Simple event-network
Let's do the above but with an event-network, to illustrate them.
A network is where all your events and behaviors live and interact with each other. They can only do that inside
Moment
monadic context. In tutorial Functional Reactive Programming kick-starter guide the analogy for event-network is a human brain. A human brain is where all event streams and behaviors interleave with each other, but the only way to access the brain is through receptors, which act as event source (input).Now, before we proceed, carefully check out the types of the most important functions of the above snippet:
Because we use the simplest UI possible — character input/output, we are going to use module
Control.Event.Handler
, provided by Reactive-banana. Usually the GUI library does this dirty job for us.A function of type
Handler
is just an IO action, similar to other IO actions such asgetChar
orputStrLn
(e.g. the latter has typeString -> IO ()
). A function of typeHandler
takes a value and performs some IO computation with it. Thus it can only be used inside an IO context (e.g. inmain
).From types it's obvious (if you understand basics of monads) that
fromAddHandler
andreactimate
can only be used inMoment
context (e.g.makeDescriptionNetwork
), whilenewAddHandler
,compile
andactuate
can only be used inIO
context (e.g.main
).You create a pair of values of types
AddHandler
andHandler
usingnewAddHandler
inmain
, you pass this newAddHandler
function to your event-network function, where you can create an event stream out of it usingfromAddHandler
. You manipulate this event stream as much as you want, then wrap its events in an IO action, and pass the resulting event stream toreactimate
.Filtering events
Now let's only output something, if user presses + or -. Let's output 1 when user presses +, -1 when user presses -. (The rest of the code stays the same).
As we don't output if user presses anything beside + or -, the cleaner approach would be:
Important functions for Event manipulations (see
Reactive.Banana.Combinators
for more):Accumulating increments and decrements
But we don't want just to output 1 and -1, we want to increment and decrement the value and remember it between key presses! So we need to
accumE
.accumE
accepts a value and a stream of functions of type(a -> a)
. Every time a new function appears from this stream, it is applied to the value, and the result is remembered. Next time a new function appears, it is applied to the new value, and so on. This allows us to remember, which number we currently have to decrement or increment.functionStream
is basically a stream of functions(+1)
,(-1)
,(+1)
, depending on which key the user pressed.Uniting two event streams
Now we are ready to implement both octaves and pitch from the original article.
Our program will output either current pitch or current octave, depending on what the user pressed. It will also preserve the value of the current octave. But wait! That's not what we want! What if we want to output both current pitch and current octave, every time user presses either a letter or + or -?
And here it becomes super-hard. We can't union 2 event-streams of different types, so we can convert both of them to
Event t (Pitch, Octave)
. But if a pitch event and an octave event happen at different time (i.e. they are not simultaneous, which is practically certain in our example), then our temporary event-stream would rather have typeEvent t (Maybe Pitch, Maybe Octave)
, withNothing
everywhere you haven't a corresponding event. So if a user presses in sequence + b - c +, and we assume that default octave is 0 and default pitch isa
, then we end up with a sequence of pairs[(Nothing, Just 1), (Just 'b', Nothing), (Nothing, Just 0), (Just 'c', Nothing), (Nothing, Just 1)]
, wrapped inEvent
.Then we must figure out how to replace
Nothing
with what would be the current pitch or octave, so the resulting sequence should be something like[('a', 1), ('b', 1), ('b', 0), ('c', 0), ('c', 1)]
.This is too low-level and a true programmer shouldn't worry about aligning events like that, when there is a high-level abstraction available.
Behavior simplifies event manipulation
A few simple modifications, and we achieved the same result.
Turn pitch Event into Behavior with
stepper
and replaceaccumE
withaccumB
to get octave Behavior instead of octave Event. To get the resulting Behavior, use applicative style.Then, to get the event you must pass to
reactimate
, pass the resulting Behavior tochanges
. However,changes
returns a complicated monadic valueMoment t (Event t (Future a))
, therefore you should usereactimate'
instead ofreactimate
. This is also the reason, why you have to liftputStrLn
in the above example twice intoeResult
, because you're lifting it toFuture
functor insideEvent
functor.Check out the types of the functions we used here to understand what goes where: