Personal tools

User:Echo Nolan/Reactive Banana: Straight to the Point

From HaskellWiki

Jump to: navigation, search


1 Introduction

So I'm writing this tutorial as a means of teaching myself FRP and reactive-banana. It'll probably be full of errors and bad advice, use it at your own risk.

All the tutorials on FRP I've read start with a long boring theory section. This is an instant gratification article. For starters, imagine a man attempting to sharpen a banana into a deadly weapon. See? You're gratified already! Here, I'll write some code for playing musical notes on your computer, attach that to reactive-banana and build increasingly complicated and amusing "sequencers" using it. Now for a boring bit:

Go install sox: apt-get install sox # Or equivalent for your OS/Distro

Get the git repository associated with this tutorial: git clone

Install reactive-banana cabal install reactive-banana

2 Musical interlude

Cd into the git repo and open rbsttp.hs in GHCi:

cd rbsttp
ghci rbsttp.hs

Now, we can make some beepy noises. Try these:

playNote (negate 5) C
playNote (negate 5) Fsharp
sequence_ . intersperse (threadDelay 1000000) $ map (playNote (negate 5)) [C ..]

Play with the value passed to threadDelay a bit for some more interesting noises. It's the time to wait between Notes, expresssed in microseconds.

sequence_ . intersperse (threadDelay 500000) $ map (playNote (negate 5)) [C ..]
sequence_ . intersperse (threadDelay 250000) $ map (playNote (negate 5)) [C ..]
sequence_ . intersperse (threadDelay 125000) $ map (playNote (negate 5)) [C ..]
sequence_ . intersperse (threadDelay  62500) $ map (playNote (negate 5)) [C ..]

You've probably figured out by now that C and Fsharp are data constructors. Here's the definition for my Note type.

-- 12 note chromatic scale starting at middle C.
data Note =
    C | Csharp | D | Dsharp | E | F | Fsharp | G | Gsharp | A | Asharp | B
    deriving (Show, Enum)

playNote is a very hacky synthesizer. It's also asynchronous, which is why mapM_ playNote (negate 5) [C ..] doesn't sound too interesting. Here's playNote's type.

-- Play a note with a given gain relative to max volume (this should be
-- negative), asynchronously.
playNote :: Int -> Note -> IO ()

3 Ground yourself, then insert the electrodes into the banana

Everything we've done so far is plain old regular Haskell in the IO monad. Try this now:

(sendNote, network) <- go1
sendNote ((negate 10), C)
sendNote ((negate 10), Fsharp)

Congratulations! You just compiled your first EventNetwork and sent your first Events. I know this looks like I just made a excessively complicated version of uncurry playNote, but bear with me for a moment. Let's look at the code for go1:

go1 :: IO ((Int, Note) -> IO (), EventNetwork)
go1 = do
    (addH, sendNoteEvent) <- newAddHandler
    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            noteEvent <- fromAddHandler addH
            reactimate $ fmap (uncurry playNote) noteEvent
    network <- compile networkDescription
    actuate network
    return (sendNoteEvent, network)

From it's type we can see that this is an IO action that returns a tuple of what is, yes, just fancy uncurry playNote and something called a EventNetwork. The EventNetwork is the new, interesting bit. The two new important abstractions that reactive-banana introduces are Events and Behaviors. Behaviors, we'll get to a bit later. Events are values that occur at discrete points in time. You can think of an Event t a(ignore the t for now) as a [(Time, a)] with the times monotonically increasing as you walk down the list.

In general, to get Events from IO we'll need to use fromAddHandler. Unsurprisingly, it wants an addHandler as its argument. Let's take a look at those types:

type AddHandler a = (a -> IO ()) -> IO (IO ())
fromAddHandler :: Frameworks t => AddHandler a -> Moment t (Event t a)

Reactive-banana makes a pretty strong assumption that you're hooking it up to some callback-based, "event driven programming" library. An AddHandler a takes a function that takes an a and does some IO and "registers the callback" and returns a "cleanup" action. Reactive-banana will hook that callback into FRP, and call the cleanup action whenever we pause our EventNetwork. (You can pause and actuate an EventNetwork as many times as you like.)

Since we don't have anything that looks like an AddHandler, we need a convenience function to make one for us. Ta-da:

newAddHandler :: IO (AddHandler a, a -> IO ())

That gives us an AddHandler and the function that triggers the Event, which we bound to the name sendNote way back when we ran go1.

go1 has two Events in it. The first is noteEvent :: Event t (Int, Note) the one you send at the ghci prompt. The second is anonymous, but it's type is Event t (IO ()). We build that one using fmap and uncurry playNote. In general, we'll be manipulating Events and Behaviors using fmap, Applicative and some reactive-banana specific combinators.

Put the weird type constraint on networkDescription out of your mind for now. The Moment monad is what we use to build network descriptions. I don't understand exactly what's going on with forall Frameworks t. => Moment t (), but it makes GHC happy and probably stops me from writing incorrect code somehow.

compile turns a network description into an EventNetwork, and actuate is fancy-FRP-talk for "turn on".

4 Plug a metronome into the banana

Since GHC has such great concurrency support, and we were already using threadDelay back in section 2, we're going to use a couple of threads and a Chan () to build and attach our metronome. Here's a function that lets us build AddHandler as out of IO functions that take Chan a as an argument.

addHandlerFromThread :: (Chan a -> IO ()) -> AddHandler a
addHandlerFromThread writerThread handler = do
    chan <- newChan
    tId1 <- forkIO (writerThread chan)
    tId2 <- forkIO $ forever $ (readChan chan >>= handler)
    return (killThread tId1 >> killThread tId2)

So, basically, we make a new Chan, forkIO the given function, passing the new Chan to it as an argument, create a second thread that triggers the callback handler whenever a new item appears on the Chan and returns a cleanup action that kills both threads. Some version of addHandlerFromThread may or may not become part of reactive-banana in the future, filing a ticket is on my to-do list.

On to the actual metronome bit:

bpmToAddHandler :: Int -> AddHandler ()
bpmToAddHandler x = addHandlerFromThread go
    where go chan = forever $ writeChan chan () >> threadDelay microsecs
          microsecs :: Int
          microsecs = round $ (1/(fromIntegral x) * 60 * 1000000)

Easy peasy. goBpm is basically the same as go1, with a different event source and fixed gain and pitch.

goBpm :: Int -> IO EventNetwork
goBpm bpm = do
    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            tickEvent <- fromAddHandler (bpmToAddHandler bpm)
            reactimate $ fmap (const $ playNote (negate 5) Fsharp) tickEvent
    network <- compile networkDescription
    actuate network
    return network

Try it out:

goBpm 240
-- Wait until you get tired of that noise
pause it

If you've gotten confused here, it is a special variable only available in GHCi, holding the return value of the last expression, and pause stops the operation of an EventNetwork.

5 Warming things up: Banana, meet Microwave

Let's play some chords instead of just single notes. First, the easy part:

-- The last two will sound ugly, but whatever I'm not an actual musician and
-- this is a tutorial.
chordify :: Note -> [Note]
chordify n = let n' = fromEnum n in map (toEnum . (`mod` 12)) [n', n'+1, n'+2]

Now how do we hook that up to FRP? We already know fmap, so we can get something of type Event t Note -> Event t [Note], but how do we get a list of Notes to play at the same time? Meet a new combinator:

spill :: Event t [a] -> Event t a

So, now we can define:

chordify' :: Event t Note -> Event t Note
chordify' = spill . fmap chordify

Integrating that into goBpm, we have:

goBpmChord :: Int -> IO EventNetwork
goBpmChord bpm = do
    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            tickEvent <- fromAddHandler (bpmToAddHandler bpm)
            let noteEvent = chordify' $ fmap (const C) tickEvent
            reactimate $ fmap (uncurry playNote . (negate 5,)) noteEvent
    network <- compile networkDescription
    actuate network
    return network

6 This banana is getting crowded! Plugging in a clock

Let's take our metronome and turn it into a beat counting metronome. Then we can play some scales and other patterns - like when we played around with threadDelay, intersperse and sequence_ back in section 2. Meet accumE:

accumE :: a -> Event t (a -> a) -> Event t a

Given an initial value and a time-stream of functions for combining values, this will emit a stream of combined values, accumulating over time. Behold:

counterify :: Event t () -> Event t Integer
counterify ev = accumE 0 (const (+1) <$> ev)

justCount :: IO EventNetwork
justCount = do
    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            beats <- fromAddHandler (bpmToAddHandler 60)
            let counting = counterify beats
            reactimate $ fmap print counting
    network <- compile networkDescription
    actuate network
    return network

This will spew numbers into your GHCi prompt, but you can still do the pause it thing to stop it counting at you.

7 Putting the banana on a diet: scales!

You can probably figure this one out yourself:

scale :: Int -> IO EventNetwork
scale bpm = do
    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            idxE <- counterify <$> fromAddHandler (bpmToAddHandler bpm)
            let notesE = (toEnum . ((`mod` 12))) . fromEnum <$> idxE
            reactimate $ fmap (uncurry playNote . (negate 5,)) notesE
    network <- compile networkDescription
    actuate network
    return network

For the next one you need to understand reactive-banana's union combinator. It just takes two events of the same type and merges them. Then we can do two scales at once!

scales :: Int -> IO EventNetwork
scales bpm = do
    let networkDescription :: forall t. Frameworks t => Moment t ()
        networkDescription = do
            idxAscE <- counterify <$> fromAddHandler (bpmToAddHandler bpm)
            let idxDscE = negate <$> idxAscE
                notesAscE = (toEnum . ((`mod` 12))) . fromEnum <$> idxAscE
                notesDscE = (toEnum . ((`mod` 12))) . fromEnum <$> idxDscE
            -- Reactive.Banana.union clashes with Prelude.union, hence RB.union
            reactimate $ fmap (uncurry playNote . (negate 5,)) $ RB.union notesAscE notesDscE
    network <- compile networkDescription
    actuate network
    return network

8 Non-conclusion

That's as far as I'm going for now. Hooking this up to keyboard input would be a logical next step, but I'm off to help my step-family move.