Why should we use Behavior in FRP

2019-03-25 17:05发布

问题:

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 Events as soon as possible?

In my little experiment I don't see where Behavior can be used.

Thanks

回答1:

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. A Behaviour has a value at all points in time, with no instants of time that are special (except with changes, 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 had Events 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 and Event 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 a Behaviour Position representing where it is at all times. Usually I'll be thinking of the object as having a position at all times, so Behaviour is a better conceptual fit.

Another place Behaviours 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 use fromPoll to make a Behaviour 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!



回答2:

Behaviors have a value all the time, whereas Events 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, and change 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.



回答3:

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 to reactimate 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 to reactimate 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, or f7. 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:

   +     -     +   -- octave stream (time goes from left to right)
     b     c       -- pitch stream

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 is 0. We should end up with an event stream that looks like this:

  a1 b1 b0 c0 c1   -- a1 corresponds to + event, b1 to b, b0 to -, etc

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:

import System.IO
import Control.Monad (forever)

main :: IO ()
main = do
  -- Terminal config to make output cleaner
  hSetEcho stdin False
  hSetBuffering stdin NoBuffering
  -- Event loop
  forever (getChar >>= putChar)

Simple event-network

Let's do the above but with an event-network, to illustrate them.

import Control.Monad (forever)
import System.IO (BufferMode(..), hSetEcho, hSetBuffering, stdin)

import Control.Event.Handler (newAddHandler)
import Reactive.Banana
import Reactive.Banana.Frameworks

makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
  event <- fromAddHandler myAddHandler
  reactimate $ putChar <$> event

main :: IO ()
main = do
  -- Terminal config to make output cleaner
  hSetEcho stdin False
  hSetBuffering stdin NoBuffering
  -- Event loop
  (myAddHandler, myHandler) <- newAddHandler
  network <- compile (makeNetworkDescription myAddHandler)
  actuate network
  forever (getChar >>= myHandler)

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:

type Handler a = a -> IO ()
newtype AddHandler a = AddHandler { register :: Handler a -> IO (IO ()) }
newAddHandler :: IO (AddHandler a, Handler a)
fromAddHandler :: Frameworks t => AddHandler a -> Moment t (Event t a)
reactimate :: Frameworks t => Event t (IO ()) -> Moment t ()
compile :: (forall t. Frameworks t => Moment t ()) -> IO EventNetwork
actuate :: EventNetwork -> IO ()

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 as getChar or putStrLn (e.g. the latter has type String -> IO ()). A function of type Handler takes a value and performs some IO computation with it. Thus it can only be used inside an IO context (e.g. in main).

From types it's obvious (if you understand basics of monads) that fromAddHandler and reactimate can only be used in Moment context (e.g. makeDescriptionNetwork), while newAddHandler, compile and actuate can only be used in IO context (e.g. main).

You create a pair of values of types AddHandler and Handler using newAddHandler in main, you pass this new AddHandler function to your event-network function, where you can create an event stream out of it using fromAddHandler. You manipulate this event stream as much as you want, then wrap its events in an IO action, and pass the resulting event stream to reactimate.

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).

action :: Char -> Int
action '+' = 1
action '-' = (-1)
action  _  = 0

makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
  event <- fromAddHandler myAddHandler
  let event' = action <$> filterE (\e -> e=='+' || e=='-') event
  reactimate $ putStrLn . show <$> event'

As we don't output if user presses anything beside + or -, the cleaner approach would be:

action :: Char -> Maybe Int
action '+' = Just 1
action '-' = Just (-1)
action  _  = Nothing

makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
  event <- fromAddHandler myAddHandler
  let event' = filterJust . fmap action $ event
  reactimate $ putStrLn . show <$> event'

Important functions for Event manipulations (see Reactive.Banana.Combinators for more):

fmap :: Functor f => (a -> b) -> f a -> f b
union :: Event t a -> Event t a -> Event t a
filterE :: (a -> Bool) -> Event t a -> Event t a
accumE :: a -> Event t (a -> a) -> Event t a
filterJust :: Event t (Maybe a) -> Event t a

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.

makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription myAddHandler = do
  event <- fromAddHandler myAddHandler
  let event' = filterJust . fmap action $ event
      functionStream = (+) <$> event' -- is of type Event t (Int -> Int)
  reactimate $ putStrLn . show <$> accumE 0 functionStream

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.

type Octave = Int
type Pitch = Char

actionChangeOctave :: Char -> Maybe Int
actionChangeOctave '+' = Just 1
actionChangeOctave '-' = Just (-1)
actionChangeOctave  _  = Nothing

actionPitch :: Char -> Maybe Char
actionPitch c
  | c >= 'a' && c <= 'g' = Just c
  | otherwise = Nothing

makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription addKeyEvent = do
  event <- fromAddHandler addKeyEvent
  let eChangeOctave = filterJust . fmap actionChangeOctave $ event
      eOctave = accumE 0 ((+) <$> eChangeOctave)
      ePitch = filterJust . fmap actionPitch $ event
      eResult = (show <$> ePitch) `union` (show <$> eOctave)
  reactimate $ putStrLn <$> eResult

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 type Event t (Maybe Pitch, Maybe Octave), with Nothing 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 is a, 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 in Event.

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.

makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t ()
makeNetworkDescription addKeyEvent = do
  event <- fromAddHandler addKeyEvent
  let eChangeOctave = filterJust . fmap actionChangeOctave $ event
      bOctave = accumB 0 ((+) <$> eChangeOctave)
      ePitch = filterJust . fmap actionPitch $ event
      bPitch = stepper 'a' ePitch
      bResult = (++) <$> (show <$> bPitch) <*> (show <$> bOctave)
  eResult <- changes bResult
  reactimate' $ (fmap putStrLn) <$> eResult

Turn pitch Event into Behavior with stepper and replace accumE with accumB 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 to changes. However, changes returns a complicated monadic value Moment t (Event t (Future a)), therefore you should use reactimate' instead of reactimate. This is also the reason, why you have to lift putStrLn in the above example twice into eResult, because you're lifting it to Future functor inside Event functor.

Check out the types of the functions we used here to understand what goes where:

stepper :: a -> Event t a -> Behavior t a
accumB :: a -> Event t (a -> a) -> Behavior t a
changes :: Frameworks t => Behavior t a -> Moment t (Event t (Future a))
reactimate' :: Frameworks t => Event t (Future (IO ())) -> Moment t ()