Package

purescript-redox

Repository
coot/purescript-redox
License
MIT
Uploaded by
coot

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).

Redox.DSL

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 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.

Incremental updates

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 dispatches on every step of the Cofree comonad:

Redox.Utils.mkIncInterp
  :: forall state f
   . (Functor f)
  => Store state
  -> Cofree f state
  -> Cofree f state

Since the interpreter is handling store updates, you can use

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))

which will not touch the store, (the P suffix stands for pure).

Store middlewares via hoisting Cofree

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.hostCofree 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).