Sunday, July 29, 2018

Using Signals as input to Halogen

As I mentioned in my last post, Signals are very easy to use with Pux because its entire architecture is built around them. However this is not the case with Halogen and I have been investigating the very latest (as yet unreleased) code from its GitHub master repository to discover what needs to be done in order to recognize signals - in my case of MIDI device connections/disconnections and MIDI events.

The state of a Halogen system progresses by means of a series of actions that are defined in our query algebra. Usually these actions emanate from Dom events but what we want to achieve is for our Signal to generate the actions instead. Our query algebra might contain the following:

  
    data Query a =
        Init a
      | HandleDeviceConnection Device a
      | HandleMidiEvent Midi.TimedEvent a
      | ..
and we want to be able to generate the HandleDeviceConnection and HandleMidiEvent actions.

Creeating EventSources

Instead of a Signal, Halogen uses an EventSource which is defined as follows:
  
    newtype EventSource m a =
      EventSource (m { producer :: CR.Producer a m Unit, finalizer :: Finalizer m })

This operates as an effect within some monad 'm' and which emits actions of type 'a'. When it is run, it returns a producer coroutine (from package purescript-couroutines) that emits these actions and the EventSource itself runs in the same monad `m`. The other part of the definition is a Finalizer which performs some sort of cleanup if the EventSource needs to be torn down after use. In our case, we don't need to do anything and so we can just use mempty (it has a Monoid instance).

In fact you are discouraged from constructing an EventSource yourself in favour of using one of the constructor functions that Halogen provides - effectEventSource, affEventSource, or eventListenerEventSource which will build one for you if you are able to provide a suitable callback function. When our Signal is run, it does so in the Effect monad and so we choose the first of these. It requires a callback of the following type where the type variable 'a' is intended to stand for our query algebra:

 
   (Emitter Effect a -> Effect (Finalizer Effect))
and so, for our Device Signal, we can simply run the signal and map it on to the required action using the emit function that Halogen provides:
 
    adaptDeviceSignal :: Signal Device -> (Emitter Effect (Query Unit) -> Effect (Finalizer Effect))
    adaptDeviceSignal sig = do
      \emitter -> do
        let
          getNext device = do
             emit emitter (HandleDeviceConnection device)
        runSignal $ sig ~> getNext
        pure mempty
where emit has the signature:
 
   emit :: forall m a. Emitter m a -> (Unit -> a) -> m Unit
We can, of course, do an entirely equivalent thing for the MIDI Event Signals with a midiEventSignal function.

Subscribing to the EventSource

Finally, we need to set up the subscription to our Event streams when the program initialises (i.e. in response to the Init action). For example:
 
    eval (Init next) = do
      -- subscribe to device connections and disconnections
      deviceSource <- H.liftEffect $ effectEventSource (adaptDeviceSignal deviceSignal)
      _ <- H.subscribe deviceSource
      -- subscribe to MIDI event messages from these devices
      midiSource <- H.liftEffect $ effectEventSource (adaptMidiEventSignal midiEventSignal)
      _ <- H.subscribe midiSource
      pure next
and we're done. Many thanks to Nicholas Scheel for pointing me towards EventSource and to the fact that it was changing in the forthcoming Halogen release.

No comments:

Post a Comment