Personal tools

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

From HaskellWiki

(Difference between revisions)
Jump to: navigation, search
(Plug a metronome into the banana)
(Plug a metronome into the banana)

Revision as of 17:52, 6 October 2012


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.

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

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

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.