Personal tools

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

From HaskellWiki

(Difference between revisions)
Jump to: navigation, search
Line 80: Line 80:
 
From it's type we can see that this is an IO action that returns a tuple of what is, yes, just fancy <code-haskell>uncurry playNote</code-haskell> and something called a <code-haskell>EventNetwork</code-haskell>. The <code-haskell>EventNetwork</code-haskell> is the new, interesting bit. The two new important abstractions that reactive-banana introduces are <code-haskell>Event</code-haskell>s and <code-haskell>Behavior</code-haskell>s. <code-haskell>Behavior</code-haskell>s, we'll get to a bit later. <code-haskell>Event</code-haskell>s are values that occur at discrete points in time. You can think of an <code-haskell>Event t a</code-haskell>(ignore the t for now) as a <code-haskell>[(Time, a)]</code-haskell> with the times monotonically increasing as you walk down the list.
 
From it's type we can see that this is an IO action that returns a tuple of what is, yes, just fancy <code-haskell>uncurry playNote</code-haskell> and something called a <code-haskell>EventNetwork</code-haskell>. The <code-haskell>EventNetwork</code-haskell> is the new, interesting bit. The two new important abstractions that reactive-banana introduces are <code-haskell>Event</code-haskell>s and <code-haskell>Behavior</code-haskell>s. <code-haskell>Behavior</code-haskell>s, we'll get to a bit later. <code-haskell>Event</code-haskell>s are values that occur at discrete points in time. You can think of an <code-haskell>Event t a</code-haskell>(ignore the t for now) as a <code-haskell>[(Time, a)]</code-haskell> with the times monotonically increasing as you walk down the list.
   
<code-haskell>go1</code-haskell> has two <code-haskell>Event</code-haskell>s in it. The first is <code-haskell>noteEvent :: Event t (Int, Note)</code-haskell> the one you send at the ghci prompt. The second is anonymous, but it's type is <code-haskell>Event t (IO ())</code-haskell>. We build that one using <code-haskell>fmap</code-haskell> and <code-haskell>uncurry playNote</code-haskell>. In general, we'll be manipulating <code-haskell>Event</code-haskell>s and <code-haskell>Behavior</code-haskell>s using <code-haskell>fmap</code-haskell>, <code-haskell>Applicative</code-haskell> and some reactive-banana specific combinators.
 
 
Put the weird type constraint on <code-haskell>networkDescription</code-haskell> out of your mind for now. The <code-haskell>Moment</code-haskell> monad is what we use to build network descriptions. I don't understand exactly what's going on with <code-haskell> forall Frameworks t. => Moment t ()</code-haskell>, but it makes GHC happy and probably stops me from writing incorrect code somehow.
 
 
<code-haskell>compile</code-haskell> turns a network description into an <code-haskell>EventNetwork</code-haskell>, and <code-haskell>actuate</code-haskell> is fancy-FRP-talk for "turn on".
 
 
== Plug a metronome into the banana ==
 
 
In general, to get <code-haskell>Event</code-haskell>s from IO we'll need to use <code-haskell>fromAddHandler</code-haskell>. Unsurprisingly, it wants an <code-haskell>addHandler</code-haskell> as its argument. Let's take a look at those types:
 
In general, to get <code-haskell>Event</code-haskell>s from IO we'll need to use <code-haskell>fromAddHandler</code-haskell>. Unsurprisingly, it wants an <code-haskell>addHandler</code-haskell> as its argument. Let's take a look at those types:
   
Line 95: Line 88:
   
 
Reactive-banana makes a pretty strong assumption that you're hooking it up to some callback-based, "event driven programming" library. An <code-haskell>AddHandler a</code> takes a function that takes an <code-haskell>a</code-haskell> 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 <code-haskell>pause</code-haskell> our <code-haskell>EventNetwork</code-haskell>. (You can <code-haskell>pause</code-haskell> and <code-haskell>actuate</code-haskell> an <code-haskell>EventNetwork</code-haskell> as many times as you like.)
 
Reactive-banana makes a pretty strong assumption that you're hooking it up to some callback-based, "event driven programming" library. An <code-haskell>AddHandler a</code> takes a function that takes an <code-haskell>a</code-haskell> 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 <code-haskell>pause</code-haskell> our <code-haskell>EventNetwork</code-haskell>. (You can <code-haskell>pause</code-haskell> and <code-haskell>actuate</code-haskell> an <code-haskell>EventNetwork</code-haskell> as many times as you like.)
  +
  +
Since we don't have anything that looks like an <code-haskell>AddHandler</code-haskell>, we need a convenience function to make one for us. Ta-da:
  +
  +
<pre-haskell>
  +
newAddHandler :: IO (AddHandler a, a -> IO ())
  +
</pre-haskell>
  +
  +
That gives us an <code-haskell>AddHandler</code-haskell> and the function that triggers the <code-haskell>Event</code-haskell>, which we bound to the name <code-haskell>sendNote</code-haskell> way back when we ran go1.
  +
  +
<code-haskell>go1</code-haskell> has two <code-haskell>Event</code-haskell>s in it. The first is <code-haskell>noteEvent :: Event t (Int, Note)</code-haskell> the one you send at the ghci prompt. The second is anonymous, but it's type is <code-haskell>Event t (IO ())</code-haskell>. We build that one using <code-haskell>fmap</code-haskell> and <code-haskell>uncurry playNote</code-haskell>. In general, we'll be manipulating <code-haskell>Event</code-haskell>s and <code-haskell>Behavior</code-haskell>s using <code-haskell>fmap</code-haskell>, <code-haskell>Applicative</code-haskell> and some reactive-banana specific combinators.
  +
  +
Put the weird type constraint on <code-haskell>networkDescription</code-haskell> out of your mind for now. The <code-haskell>Moment</code-haskell> monad is what we use to build network descriptions. I don't understand exactly what's going on with <code-haskell> forall Frameworks t. => Moment t ()</code-haskell>, but it makes GHC happy and probably stops me from writing incorrect code somehow.
  +
  +
<code-haskell>compile</code-haskell> turns a network description into an <code-haskell>EventNetwork</code-haskell>, and <code-haskell>actuate</code-haskell> is fancy-FRP-talk for "turn on".
  +
  +
== Plug a metronome into the banana ==
   
 
Since GHC has such great concurrency support, and we were already using <code-haskell>threadDelay</code-haskell> back in section 2, we're going to use a couple of threads and a <code-haskell>Chan ()</code-haskell> to build and attach our metronome. Here's a function that lets us build <code-haskell>AddHandler a</code-haskell>s out of IO functions that take <code-haskell>Chan a</code-haskell> as an argument.
 
Since GHC has such great concurrency support, and we were already using <code-haskell>threadDelay</code-haskell> back in section 2, we're going to use a couple of threads and a <code-haskell>Chan ()</code-haskell> to build and attach our metronome. Here's a function that lets us build <code-haskell>AddHandler a</code-haskell>s out of IO functions that take <code-haskell>Chan a</code-haskell> as an argument.

Revision as of 06:04, 7 October 2012

Contents

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 https://github.com/enolan/rbsttp.git

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.

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