A monad for reactive programming. Part 1

As of March 2020, School of Haskell has been switched to read-only mode.

For the impatient: you can find something runnable at the bottom.

Functional reactive programming has no notion of event scope. A functional, declarative, reactive computation is affected as a whole by a signal, and must re-start from the beginning in some way, since it is declarative. A monad can contain the scope of the signal, so part of the computation already done upstream could be kept when a signal is injected at some level of the computation by an active component.

A monad can decompose the computation in a chain of event handlers virtually set up by the monadic computation when it is called by the top signal, that is, when it is run for the first time.

This has many applications. Imagine this computation:

profits = do
    quantity <- waitEvent "quantity"
    liftIO $ do
        putStr "quantity="
        print quantity
        getChar
    price <- waitEvent "price"
    liftIO $ do
        putStr "price="
        print price
        getChar
    liftIO $ do
        putStr $ "total="
        print $ quantity * price
        getChar
    return $ quantity * price

Suppose that quantity and price changes along the day, so there are two events, one when the stock sold (quantity changes) and one when price changes. Suppose that, instead of printing in the console, the process update a display in an executive dashboard and the computation is much more complicated and costly with access to databases and OLAP reports, with a lot of different events. We need only that when the price changes, the monad execute just the code needed to change the price and the profits without touching the other costly branches of the monad, including not only the part of the computation that is above, upstream, but also some branches that may be after the event, if we wish.

A monad suitable for configuring event handlers when it is executed is the one below:

data Transient m x = Transient (m (Maybe x))

instance (MonadState m, StateType m ~ EventF) => Monad (Transient m) where
    return x = Transient $ return $ Just x
    x >>=  f = Transient $ do
        setEventCont f
        mk <-  runTrans x
        case mk of
            Just k  ->  runTrans $ f k
            Nothing -> return Nothing

The name Transient comes from my old idea of closures in a monadic expression as transient pure states that are only re-evaluated when some change appears upstream.

Let's use following primitive to tell the rest of the computation downstream to stop:

stop = empty

empty :: Monad m => Transient m a
empty = Transient $ return Nothing

This empty will be the empty of the applicative instance for Transient, but I don´t want to introduce it right now.

It uses the state monad since we need to "transport" EventF data. This kind of data are continuations (f) for each computation (x).

data EventF = forall m b c. EventF (b -> Transient m c)

Transient uses Maybe because I want to stop the computation to flow down whenever further computations are not necesssary. The monad does not install event handlers, it simply store the next continuation with setEventCont:

setEventCont f = do
    f' <- get
    put $ EventF f
    return f'

The real computation that set the event handlers is waitEvent, it is an active element that set up an event handler. That event handler is active element's continuation (set by the monad) for the event that is waiting.

(Note: I could have done it more simple obviating the state but as we should see, the final solution need it).

At the end, waitEvent stop the computation (returning Nothing):

waitEvent name = Transient $ do
    f   <- get
    evs <- liftIO $ takeMVar eventHandlers
    liftIO . putMVar eventHandlers . M.insert name f $ evs  -- !> ("set "++name)
    return Nothing
    ```

It uses a map of event handlers:

``` haskell
eventHandlers ∷  MVar (M.Map String EventF)
eventHandlers= unsafePerformIO $ newMVar M.empty

Then, after the monadic expression is executed, this structure has at least the first event handler set. eventLoop waits for events and executes the corresponding event handlers:

type EvType = String
data Event  = forall a. Event EvType a

eventLoop [] = return ()
eventLoop (Event name r : evs) = do
    liftIO . print $ "new event: " ++ name
    ths <- liftIO $ readMVar eventHandlers
    case M.lookup name ths of
      Just (EventF f) -> runTrans'' $ (unsafeCoerce f) r
      Nothing -> return ()
    eventLoop evs

An Event contains its type (that is used to lookup for event handler) and its value.

Instead of a list, eventLoop could be defined as a process that reads a queue.

Note the unsafeCoerce there. By construction I know that it must work, since it applies the x of the monad with the f of the continuation stored in the state, but the compiler has no such information. For this time, as a matter of experiment I will allow it. I'm a physicist, not an mathematician neither an engineer. If I were a mathematician I would spent three years struggling with the denotational semantics of event handling, producing abstruse papers before doing anything usable. If I were an engineer I would do a visual basic program for the concrete problem. I love math and engineering, but I would happily sacrifice conceptual beauty and delivery time to get the general problem solved.

runTrans'' is the computation that executes the continuation assuming a StateT transformer that transports the EventF state.

runTrans'' :: Transient (StateT EventF IO) a → IO ()
runTrans'' tmx = runTrans' tmx >> return ()

runTrans' ::  Monad m => Transient (StateT EventF m) x -> m (Maybe x)
runTrans' tmx = evalStateT (runTrans tmx) undefined

The continuation called by runTrans'' may contain further waitEvent sentences that will add further event handlers to the loop by the same mechanism.

Let´s give a set of events to the monster:

eventList =
    [ Event "quantity" 10
    , Event "price"     2
    , Event "price"     3
    , Event "quantity" 30
    , Event "price"     4
    ]

And run it (press Enter to make advance the computation, since profits has getChar).

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE UndecidableInstances      #-}
{-# LANGUAGE MultiParamTypeClasses     #-}

import Control.Concurrent.MVar
import Control.Monad.State
import Data.Map                as M
import System.IO.Unsafe
import Unsafe.Coerce


data EventF = forall m b c. EventF (b -> Transient m c)
data Transient m x = Transient (m (Maybe x))

runTrans :: Transient m x -> m (Maybe x)
runTrans (Transient mx) = mx

setEventCont f = do
    f' <- get
    put $ EventF f
    return f'

empty :: Monad m => Transient m a
empty = Transient $ return Nothing

instance (MonadState EventF m) => Monad (Transient m) where
    return x = Transient . return . Just $ x
    x >>= f = Transient $ do
        setEventCont f
        mk <- runTrans x
        case mk of
          Just k  -> runTrans (f k)
          Nothing -> return Nothing

instance MonadTrans Transient where
    lift mx = Transient $ mx >>= return . Just

instance (MonadState EventF m, MonadIO m) => MonadIO (Transient m) where
    liftIO io = let x = liftIO io in x `seq` lift x

instance (MonadState EventF m) => MonadState EventF (Transient  m) where
--  type StateType (Transient m) = EventF
    get = Transient $ get >>= return . Just
    put x = Transient $ put x >> return (Just ())

eventHandlers :: MVar (M.Map String EventF)
eventHandlers = unsafePerformIO $ newMVar M.empty

type EvType = String
data Event = forall a. Event EvType a

waitEvent name = Transient $ do
    f <- get
    evs <- liftIO $ takeMVar eventHandlers
    liftIO . putMVar eventHandlers . M.insert name f $ evs
    return Nothing

eventLoop [] = return ()
eventLoop (Event name r : evs) = do
    liftIO . putStrLn $ "new event: " ++ name
    ths <- liftIO . readMVar $ eventHandlers
    case M.lookup name ths of
      Just (EventF f) -> runTrans'' $ (unsafeCoerce f)  r
      Nothing         -> return ()
    eventLoop evs

runTrans' :: Monad m => Transient (StateT EventF m) x -> m (Maybe x)
runTrans' tmx = evalStateT (runTrans tmx) undefined

runTrans'' :: Transient (StateT EventF IO) a -> IO ()
runTrans'' tmx = runTrans' tmx >> return ()

-- show
main = do
  runTrans'' profits
  eventLoop eventList
  putStrLn "END"

eventList :: [Event]
eventList =
    [ Event "quantity" 10
    , Event "price"     2
    , Event "price"     3
    , Event "quantity" 30
    , Event "price"     4
    ]

profits :: Transient (StateT EventF IO) Integer
profits = do
    quantity <- waitEvent "quantity"
    liftIO $ do
        putStr "quantity="
        print quantity
        getChar
    price <- waitEvent "price"
    liftIO $ do
        putStr "price="
        print price
        getChar
    liftIO $ do
        putStr $ "total="
        print $ quantity * price
        getChar
    return $ quantity * price

-- /show

You see that only the code after each event is executed and that the context upstream necessary for the continuation is maintained when each event is called.

And that's all.

No. Look at this other example.

main = do
    runTrans'' $ do
    let threshold = 100
    pr <- profits
    liftIO $ do
        when (pr > threshold) $
            putStr "Passed threshold! Mail sent to boss."
        print pr
    eventLoop eventList
    putStrLn "END"

It may run the same profits computation and the same events, but at the end of it if a threshold of profits is surpassed, it has to produce an extra message.

If you change the main expression in the program by the above one, you would expect that the last message should be

"Passed threshold!…"

but that is not the case. This means that the monad, as is defined, does not trigger the entire set of event handlers necessary. It is necesary to store in the state not the individual continuation but a stack of all the nested continuations in all subcomputations that are affected by the event.

Moreover, the unsafeCoerce may produce segmentation faults, if instead of

waitEvent "quantity"

alone in a monadic computation we'll put

(*) <$> return units <*> waitEvent "quantity"

.

But this in the second part.

By the way the improvement of this monad that I will present in the second part is the one used in hplayground, a client-side framework.