Alright, after some research and digging I’ve figured it out! Here is the demo: https://try.purescript.org/?gist=0270c9fd41d1d9643387f15fc0039ca1
main = HA.runHalogenAff do
body <- HA.awaitBody
clickCountRef <- H.liftEffect $ Ref.new 0
let
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
where
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
runUIonly accepts and returns anAff, theConsumerthatio.subscribetakes has to beConsumer i Aff a. This meant I had to transformAppMtoAffbefore callingio.subscribe, and the only place I could do that was betweensubscribeandconsumer. -
Because
Refuses a mutable JavaScript object, I can callrunAppMtwice and they will still share state, as long as both calls get the sameenvironment. 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:
-
JordanMartinez - A guide to
purescript-coroutines- What coroutines are, how they work, and their relationship toFreeT. -
purescript-halogenGithub issue: Confused when using StateT Aff as my component’s monad #386 - WhyrunUIcan only returnAff. -
Halogen.VDom.Driver - Type signatures of
runUIandHalogenIO. -
Effect/Ref.js - How Ref is implemented on the JS side.
The Halogen Guide and Real World Halogen were also great references.