Livecoding in Midair

Tom Murphy

FARM 2016

Livecoding

Two Choices

"But I want to see the world!"

Aha, FRP!

Hotswap?

Being Declarative

   fmap :: Functor f => (a -> b) -> f a -> f b
   chars = "midair" :: [Char]

   fmap (=='a') chars :: [Bool]
   getChar :: IO Char

   fmap (=='a') getChar :: IO Bool

Pseudocode:

   lastKeyPressed :: FRP Char

   fmap (=='a') lastKeyPressed :: FRP Bool

This Talk

Signals and Signal Flow

   data SFlow a b

Functions

   data SFlow a b
   sMap :: (a -> b) -> SFlow a b

   sFold :: c -> (a -> c -> c) -> SFlow a c

   sFilter :: (b -> Bool) -> SFlow b b

   sCompose :: SFlow b c -> SFlow a b -> SFlow a c

   -- ...

instance Applicative (SFlow a)
instance Arrow SFlow
   sFold True (\_ -> not) <<< sFilter (==' ')
      :: SFlow Char Bool

(demo!)

   width  :: SFlow x Double
   height :: SFlow x Double

   area :: SFlow x Double
   area = (*) <$> width <*> height

   -- Note it's deterministic

-XApplicativeDo

   do w <- width
      h <- height
      pure (w * h)

First-Class Hot-Swap

   data Direction = Up | Down

   volChange :: Double -> Direction -> Double
   volChange v Up   = v * 1.01
   volChange v Down = v * 0.99

So they play for a while...

   buttonHistory :: [Direction] -- 100 up, 50 down

   foldl volChange 1 buttonHistory == 1.65...

But that's too slow

   volChange' v Down = v * 0.97
   volChange' v Up   = v * 1.03

   foldl volChange' 1 buttonHistory == 4.19...

Design Goals

Efficiency

"Compiled code is roughly 10x faster than interpreted code, but takes about 2x longer to produce (perhaps longer if optimisation is on)." - From the GHC Users' Manual

Internals

(Simplified:)

   data SFlow a c where

      SF_Map :: (a -> c) -> SFlow a c

      SF_Compose
         :: forall a b c.
         SFlow b c -> SFlow a b -> SFlow a c

      SF_Fold :: c -> (a -> c -> c) -> SFlow a c

      SF_Filter
         :: Maybe a -> (a -> Bool) -> SFlow a a

      SF_NodeRef
         :: TVar (Maybe a) -> TVar (Maybe c)
         -> TVar (SFlow a c) -> SFlow a c

Fire

fireGraph :: TVar (SFlow i o) -> i -> STM (Maybe o)

Hot-Swap

   mkNodeRef :: SFlow a b -> IO (SFNodeRef a b)

   nRef :: SFNodeRef a b -> SFlow a b

   hotswap :: SFNodeRef a b
           -> (Maybe a -> Maybe b -> SFlow a b)
           -> IO ()

Node Refs (II)

fmap nRef (mkNodeRef x)

/=

pure x
   ref <- mkNodeRef $ sMap (transpose 2)
      :: IO (SFNodeRef Chord Chord)
   sMap playChord <<< nRef ref <<< sMap makeChord
      :: SFlow Note (IO ())
   hotswap ref $ \_ _ -> sMap (transpose 4)
   ref <- mkNodeRef $ sFold 1 volChange

   hotswap ref $ \_ lastOut ->
      sFold (fromMaybe 1 lastOut) volChange'
   hotswap ref $ \_ (Just lastOut) ->
      sFold lastOut volChange'

Idioms

   hotswapFold ref 1 volChange'
-- | Gives the first item that passes
sFind :: (a -> Bool) -> SFlow a a

sTake :: Int -> SFlow a [a]

sAll :: (a -> Bool) -> SFlow a Bool

sElem :: Eq a => a -> SFlow a Bool

sMapMaybe :: (a -> Maybe b) -> SFlow a b

sRights :: SFlow (Either l r) r

sPrint :: Show a => SFlow a (Fx ())

sSum :: Num n => SFlow n n

sMax :: Ord a => SFlow a a

hotswapHold :: SFNodeRef x a -> IO ()

hotswapFold :: SFNodeRef a c -> c -> (a -> c -> c) -> IO ()

hotswapCount :: SFNodeRef x n -> IO ()

-- ...

We can demonstrate its use using sCount:

   sCount :: Num n => SFlow x n

      == sSum <<< sConst 1

      == sFold (+) 0 <<< sMap (const 1)

We can, given keyboard input, count every key press of a digit character:

   ref <- mkNodeRef sCount

   nRef ref <<< sFilter isDigit
      :: SFlow Char Int

Then stop counting for a while:

   hotswapHold ref

Then, after more time, start counting where we left off:

   hotswapCount ref

Even after switching back to counting fires with "count," none of the key presses that occurred while the "hold" was in effect are part of the sum emitted from the graph.

Demos

Thank you!