Accessing global environment outside of Halogen component?

I’m trying to make a Halogen app (like the Real World example) that runs in a custom application monad. I have the basics working in this example:

It’s the Halogen basic example (one button that toggles state) with a global state added. In this example, the global state is the number of times the button has been clicked, but the component doesn’t actually know that - its input is an initial value (s), a render function (s -> String), and an update function (s -> s). That is: "", identity, (_<>"a") should work just as well as 0, show, (_+1).

How can I access the state in main as well as in the component? I’d like to be able to call getState and putState in, for example:

  • a function passed to io.subscribe to respond to output from the component

  • events attached to the window or document

I tried replacing io <- runUI rootComponent initExternal body with this:

runAppM environment do
  io <- H.lift $ runUI component initExternal body
  H.lift $ io.subscribe $ CR.consumer $ case _ of
    Changed -> do
      _ <- getState
      pure Nothing

but I think it’s not working because I need to convert an AppM or a FreeT or an Aff into one of the others, and that’s where I got stuck.

The error is:

Could not match type Aff with type Int while trying to match type t1 Aff with type AppM Int

And if I take out the lifts it’s:

Could not match type Aff with type AppM Int while trying to match type Aff t1 with type AppM Int t0

Alright, after some research and digging I’ve figured it out! Here is the demo:

main = HA.runHalogenAff do
  body <- HA.awaitBody
  clickCountRef <- H.liftEffect $ 0

    environment :: Env ExternalState
    environment = { state: clickCountRef }
    rootComponent :: forall q. H.Component HH.HTML q (Input ExternalState) Output Aff
    rootComponent = H.hoist (runAppM environment) component

  io <- runUI rootComponent initExternal body
  io.subscribe $ CR.consumer $ runAppM environment <<< subscribe
  pure unit
    subscribe :: forall r. Output -> AppM ExternalState (Maybe r)
    subscribe = case _ of
      Changed -> do
        state <- getState
        liftEffect $ logShow state
        pure Nothing

The two things that tie it all together are:

  • Since runUI only accepts and returns an Aff, the Consumer that io.subscribe takes has to be Consumer i Aff a. This meant I had to transform AppM to Aff before calling io.subscribe, and the only place I could do that was between subscribe and consumer.

  • Because Ref uses a mutable JavaScript object, I can call runAppM twice and they will still share state, as long as both calls get the same environment. I’ll admit I feel a little weird using mutable state, but it seems to be the recommended strategy in Real World Halogen.

These resources were extremely helpful in deepening my understanding of how coroutines, Halogen, natural transformations, Aff, and Refs all fit together:

The Halogen Guide and Real World Halogen were also great references.