haskellfrpreactive-banana

Execute MonadIO action inside of reactimate


In reactive-banana, I am trying to run reactimate :: Event (IO ()) -> Moment () with some actions of Arduino in hArduino package, an instance of MonadIO. There seems no function of Arduino a -> IO a provided in the package. How would you execute Arduino actions in reactimate?


Solution

  • How would you execute Arduino actions in reactimate?

    I would cause them to be executed indirectly, by executing an IO action which has an observable side-effect. Then, inside withArduino, I would observe this side-effect and run the corresponding Arduino command.

    Here's some example code. First, let's get the imports out of the way.

    {-# LANGUAGE GeneralizedNewtypeDeriving, ScopedTypeVariables #-}
    
    import Control.Monad.IO.Class
    import Data.IORef
    import Data.Word
    import Reactive.Banana
    import Reactive.Banana.Frameworks
    import Text.Printf
    

    Since I do not have an arduino, I'll have to mock up a few methods from hArduino.

    newtype Arduino a = Arduino (IO a)
      deriving (Functor, Applicative, Monad, MonadIO)
    
    newtype Pin = Pin Word8
    
    pin :: Word8 -> Pin
    pin = Pin
    
    digitalWrite :: Pin -> Bool -> Arduino ()
    digitalWrite (Pin n) v = Arduino $ do
        printf "Pretend pin %d on the arduino just got turned %s.\n"
               n (if v then "on" else "off")
    
    digitalRead :: Pin -> Arduino Bool
    digitalRead (Pin n) = Arduino $ do
        printf "We need to pretend we read a value from pin %d.\n" n
        putStrLn "Should we return True or False?"
        readLn
    
    withArduino :: Arduino () -> IO ()
    withArduino (Arduino body) = do
        putStrLn "Pretend we're initializing the arduino."
        body
    

    In the rest of the code, I'll pretend that the Arduino and Pin types are opaque.

    We'll need an event network to transform input events representing signals received from the arduino into output events describing what we want to send to the arduino. To keep things extremely simple, let's receive data from one pin and output the exact same data on another pin.

    eventNetwork :: forall t. Event t Bool -> Event t Bool
    eventNetwork = id
    

    Next, let's connect our event network to the external world. When output events occur, I simply write the value into an IORef, which I'll be able to observe later.

    main :: IO ()
    main = do
        (inputPinAddHandler, fireInputPin) <- newAddHandler
        outputRef <- newIORef False
    
        let networkDescription :: forall t. Frameworks t => Moment t ()
            networkDescription = do
                -- input
                inputPinE <- fromAddHandler inputPinAddHandler
    
                -- output
                let outputPinE = eventNetwork inputPinE
    
                reactimate $ writeIORef outputRef <$> outputPinE
        network <- compile networkDescription
        actuate network
    
        withArduino $ do
          let inputPin  = pin 1
          let outputPin = pin 2
    
          -- initialize pins here...
    
          -- main loop
          loop inputPin outputPin fireInputPin outputRef
    

    Note how reactimate and compile are only called once, outside the main loop. Those functions setup your event network, you do not want to call them on every loop.

    Finally, we run the main loop.

    loop :: Pin
         -> Pin
         -> (Bool -> IO ())
         -> IORef Bool
         -> Arduino ()
    loop inputPin outputPin fireInputPin outputRef = do
        -- read the input from the arduino
        inputValue <- digitalRead inputPin
    
        -- send the input to the event network
        liftIO $ fireInputPin inputValue
    
        -- read the output from the event network
        outputValue <- liftIO $ readIORef outputRef
    
        -- send the output to the arduino
        digitalWrite outputPin outputValue
    
        loop inputPin outputPin fireInputPin outputRef
    

    Note how we use liftIO to interact with the event network from inside an Arduino computation. We call fireInputPin to trigger an input event, the event network causes an output event to be triggered in response, and the writeIORef we gave to reactimate causes the output event's value to be written to the IORef. If the event network was more complicated and the input event did not trigger any output event, the contents of the IORef would remain unchanged. Regardless, we can observe that contents, and use it to determine which Arduino computation to run. In this case, we simply send the output value to a predetermined pin.