purescript-halogen-example
purescript-halogen-example copied to clipboard
Sample halogen app that uses a few DSLs within the application's free monad
Purescript Halogen Example
Sample Halogen app with a few DSLs implemented as the application's reader monad.
The overview presented here has some simplified examples. You will find some of the types presented here have less arguments / constructors / etc. than the actual types used in the source code.
You can check out the result here.
Purpose
This repository aims to be an example of how to use an application monad with Halogen
in order
to create a DSL that can be used inside the component's eval
.
In this repository, you'll find examples for DLS's that allow you to:
- read some static environment (through
MonadAsk
) - navigate to different routes, along with a routing component
- read or modify the global state of the application
- run queries on a (dummy) server API
- trigger the root component to show a dialog box and execute commands depending on action taken
Our application's DSLs
Each DSL is expressed as a typeclass
. These DSLs can define any number of functions, each of
which should return a monadic result. For example, our Navigation
DSL is defined as:
class Monad m <= NavigationDSL m where
navigate :: Route -> m Unit
The navigate
function will most likely incur a side-effect, so we express that by returning m Unit
.
In order to be able to use these DSLs in Halogen
, we need to lift these operations to its monad, which is HalogenM
:
instance navigationDSLHalogenM :: NavigationDSL m => NavigationDSL (HalogenM s f g p o m) where
navigate = lift <<< navigate
What this basically says is that whenever you use navigate
within a HalogenM context, we will
lift the DSL to the inner-monad m
, which means we'll need to have an instance ourselves.
The Reader Pattern
We will be using the _ReaderT pattern. If you have not used ReaderT
before, please go through
The ReaderT design pattern post.
The env type we are using is
type Environment =
{ token :: APIToken
, push :: PushType -> Effect Unit
, answer :: Int
, state :: Ref GlobalState
}
These are needed for all the various DSLs we are using. Specifically, the ServerDSL
uses the token
,
NavigationDSL
and DialogDSL
use the push
field to request a route change or displaying the dialog box,
and the global StateDSL
uses state
.
Our application's monad
We can define our monad as:
newtype ExampleM a = ExampleM (ReaderT Environment Aff a)
We can now derive instances for Functor
, Apply
, Applicative
, Bind
, Monad
, MonadEffect
,
and MonadAff
for free.
We also need the NavigationDSL
instance for ExampleM
:
instance navigationDSLExampleM :: NavigationDSL ExampleM where
navigate route = ExampleM do
env <- ask
liftEffect $ env.push $ PushRoute route
We basically use our environment's push
to send the new route to the event listener.
Natural transform run
Halogen needs to know how to effectively run our monad, which is expressed by its hoist
function:
hoist
:: forall h f i o m m'
. Bifunctor h
=> Functor m'
=> (m ~> m')
-> Component h f i o m
-> Component h f i o m'
What this does, basically, is given a component that runs under monad m
and a way to go
from m
to m'
(through the natural transform m ~> m'
), then we can construct the
component that runs under monad m'
.
It's also worth noting that runUI
assumes a component that runs under the Aff
monad,
so that means m'
needs to be Aff
. And since m
is our own monad, ExampleM
, it
follows we will write:
runExampleM :: forall a. ExampleM a -> Environment -> Aff a
runExampleM m env = runReaderT (unwrap m) env
All that remains is somehow figure out how to do the route change through an Eff
or Aff
.
Which brings us to...
Signaling back to main
Some of our DSLs might need to signal back to main. The basic idea is we create a new event in
main
and we pass the function that can push
to this event in the environment
to our
runExampleM
transform. This means we'll have a way of sending messages to our main
.
Back in main
, we'll have to handle them somehow. And since we also create the root component
there, we could use its driver's query to send actions to that component.
Back in main:
main = HA.runHalogenAff do
body <- HA.awaitBody
state <- liftEffect $ Ref.new 0
event <- liftEffect create
let environment =
{ token: APIToken secretKey
, answer: 42
, state
, push: event.push
}
let router' = H.hoist (flip runExampleM environment) R.component
driver <- runUI router' unit body
liftEffect $ subscribe event.event (handler driver)
We omitted the definition for handler
for brevity. You can check the Main.purs
file for details.
The main idea is we create the environment
, which has everything ExampleM
needs to run everything
we care about.
The handler
function sends messages to the router
(the main Halogen component), depending on its
input (one message is for changing the current route, and the other is for showing a dialog box).
MonadAsk
MonadAsk
allows us to get the current environment. This is initialized in main
and
passed down to our runExampleM
function.
StateDSL
Unfortunately, we can't use MonadState
because HalogenM
already has an instance for it
for each component's state. We need to define our own state monad, and one way is presented
in the StateDSL
class.
ServerAPI
We have a dummy API
method in the Example.Server.ServerAPI
module, but it could
be a function that does an Ajax
request just as well. We assume our API needs a
token, but our DSL does not ask for it.
ShowDialog
This is an example where we pass actions
as our ExampleM
monad. Basically, we
want our root component to show a dialog with a custom set of buttons. Each of
these buttons has an action that will run under ExampleM
, which means it can run
all the DSLs that we define.
The way this works is we initially send these actions as the dialog options,
under any monad m
. The ExampleM
instance for DialogDSL
assumes
both the monad that it runs under and the monad used to run the actions
is ExampleM
. It transforms the options to Aff
and pushes them to the router
through the handler stored in the environment
.
Parallel
@thomashoneyman kindly contributed a version which enables parallel computations to the application's monad. You can check it out in this PR.