Redox - since it mixes well with Thermite ;)
This is redux
type store, but instead of forcing you to write interpreter
as a reducer you are free (or rather cofree ;)) to write it the way you want.
The library will give you different schemes how the store is updated. Now
there is only one Redox.DSL
, but in near future there will be at least one
more using coroutines (similar to how
Thermite updates react
component state).
A DSL has to be interpreted in the Aff
monad. Since Aff
has an instance of
MonadEff
this does not restrict you in any way. Checkout tests how to write
synchronous and asynchronous commands
In general if your DSL
is generated by a functor C
(for commands):
type DSL = Free C
then you have to find a functor RunC eff
which pairs with C
:
pair :: forall eff x y. C (x -> y) -> RunC eff x -> Aff eff y
You can deduce from pair
's type that if C
is a sum type then RunC
is
a product - that's how it interprets C
.
Then the interpreter has type:
type Interp eff a = Cofree (RunC eff)
This give rise to a function
runInterp :: forall state. DSL(state -> state) -> RunC eff state -> Aff eff state
runInterp cmds state = exploreM pair cmds $ mkInterp state
You can feed this function into Redox.DSL.dispatch
:
dispatchS :: forall eff state. DSL(state -> state) -> Aff (redox :: Redox | eff) state
dispatchS = Redox.DSL.dispatch (\_ _ -> pure unit) runInterp store
Check out tests for an example or this
repo. However you can write
an interpreter without Cofree
, simply by using State
to track the state, or
just by hand. The advantage of using Cofree
is that whenever you will change
C
the compiler will force you to update RunC
in compatible way.
The Redox.DSL.dispatch
function will dispatch changes to the store when the
Aff
computation resolves. You may want to dispatch every node of your
interpreter i.e. when each DSL command is run in the do
block`. For example
if you try to
dispatch do
cmd1 arg1
cmd2 arg2
The dispatch
will update the store when cmd2 finishes. But you can build
this into the interpreter. Since this is common, there is a function in Redox.Utils
to
modify an interpreter of type Cofree f a
so that it updates the store on
every step of the Cofree
comonad:
Redox.Utils.mkIncInterp
:: forall state f
. (Functor f)
=> Store state
-> Cofree f state
-> Cofree f state
Note that this function will not dispatch subscriptions. If you build that
into your interpreterer or you can use dispatchP
which does not run
subscriptions (the P
suffix stands for pure).
Redox.DSL.dispatchP
:: forall state dsl eff
. (Error -> Eff (redox :: REDOX | eff) Unit)
-> (dsl -> state -> Aff (redox :: REDOX | eff) state)
-> Store state
-> dsl
-> Eff (redox :: REDOX | eff) (Canceler (redox :: REDOX | eff))
You can modify your interpreter using
Redox.Utils.hoistCofree'
:: forall f state
. (Functor f)
=> (f (Cofree f state) -> f (Cofree f state))
-> Cofree f state
-> Cofree f state
This is a version of Control.Comonad.Cofree.hoistCofree
but here the first
argument does not need to be a natural transformation. This let you add
effects to the interpreter. For example mkIncInterp
is build using it.
Another example is to add a logger.
addLogger
:: forall state f
. (Functor f)
=> Cofree f state
-> Cofree f state
addLogger interp = hoistCofree' nat interp
where
nat :: f (Cofree f state) -> f (Cofree f state)
nat fa = g <$> fa
g :: Cofree f state -> Cofree f state
g cof = unsafePerformEff do
-- Control.Comonad.Cofree.head
log $ unsafeCoerce (head cof)
pure cof
There are plenty of other things you can do with the interpreter in this way, e.g. undo/redo stack, optimistic updates, crash reporting, delay actions (or just some actions, via prisms).