I should've call the article ”The Elm Architecture in PureScript”, but both the inner demons and the inner angels said "Oh, dear, no!". Still I can't deny the article covers such topic as well.
Among that, this article covers the easiest way for accumulating errors and recovering to the last successful state of the application in a flick of the wrist (I love that song).
Also I plan to abandon explaining why I chose PureScript and get straight to the details. Just keep in mind that Haskell and PureScript are bro-s: each one is pretending to be radically different, but they share truly a lot of common.
The project I am working on, Noodle, has quite complex UI and API as well. For some time I struggled in finding the proper architecture to define both of them, because my intuition strictly said there are common patterns I am yet too blind to see. In pure functional languages it is really easy to write some shitty code, just not making it public, and reason about it later, while you have everything working and stable. And not to confuse you with the harsh word shitty, I have a bunch of unit tests to ensure the logic is what I expect it to be. Also, I think, in leaning to perfection it is important to know where to do a step off the road for some time and get back to this infinite road with new strengths and visions… but enough morals.
When I discovered the article Finite State Machines in Haskell and read it, everything stood on its place. This was the pattern! Not to oblige you reading the article, the key point is that Finite State Machine is as easy as this function:
finiteStateMachine
:: forall action model
. action -> model -> model
That is our famous ”The Elm Architecture” in its essence, simplified to the origins where it was actually born.
The function, which receives some action (say, DecrementCounter
or WithdrawMoney
or JustRobThePlace
), gets previous model and evaluates to the new modified model. Some people prefer it flipped, so the model comes as the first argument. Those actions are usually the sum types so it is easy to keep them in one place and have a birds-eye view on the processes happening in your application. 1
Let’s move the practical example to another part of the article and for now mostly speak theory. Don’t be confused, in the end of this part I’ll give the complete working code for FSM using FRP, since it’s just a few lines of code. First, we will define the signature, then the way to make
it and, finally, run
it.
Considering Effects
Both the UI and API in my case require some side effects to be performed. For API it’s, for example, generating a random UUID for any new entity. And, since I use FRP events, subscribing to event streams or cancelling them is also a side effect. In case of UI, it is Event.stopPropagation
(where event is HTML event, not the FRP one), requestAnimationFrame
, drawing on canvas, some connections to JavaScript using FFI. Some of such effects should be Aff
though, since they are asynchronous, but it doesn’t really affect what we discuss in any matter.
Separating effects from the pure actions is very important in pure functional languages, still it doesn’t mean you can’t use them, just keep them marked as such and, preferably, sorted (see below).
finiteStateMachine
:: forall action model
. action -> model -> Effect model
In Elm, every update
function returns Cmd
along with the model, which is also the wrapper for the effects, still you may need some effort to realise it. Actually, they separate model from the effects, and that’s smart thing to do, so let’s do the same:
finiteStateMachine
:: forall action model
. action -> model -> model /\ Effect Unit
If you ever used the Spork
library, you may want to draw attention to the fact that they also use a sum type to describe effects. So in my case they would have NewUUID
and StopPropagation
constructors for the effect
type instances and functions like:
finiteStateMachine
:: forall action model effect
. action -> model -> model /\ List effect
performEffect
:: forall effect
. effect -> Effect Unit
Where performEffect
is performed after every call to FSM. That’s a really good way to sort the effects and don’t put them into one basket. But I decided not to do that in the end: I had such code before and may be I’ll get it back in some form, but during the rewriting stage I wanted not to bother thinking about that, and now I have the working code, which is open to return to this approach, at some point, maybe. Just please notice that it’s really something to think through.
Actually the Elm’s Cmd
is doing the similar thing by requiring you to provide functions which return specific actions, but more on that later, just in the next chapter.
If you have considered that, let’s get back to the latest specification and stick with it in the end of this section:
finiteStateMachine
:: forall action model
. action -> model -> model /\ Effect Unit
The article on Haskell’s way covers it really well with the examples using IO
, which is the same concept as Effect
in PureScript.
Recursing to Actions
If you are willing to keep actions minimalistic, meaning that each action is modifying the model in one single and predictable way, defined by the name of such action (e.g. not just RobTheBank
, but rather splitting it into BuyToyShotgun
, DefineWorkingHours
, FindOutSecuritySchedule
, WearMask
, EnterTheBank
, GetToCachier
, TryNotToPee
and so on, it’s not me to give you details on how to do it, just visit wikihow.com
on robbing banks), you would see that one action sometimes requires other actions to be performed. Without wearing a mask, it’s better not to start the robbing procedure at all (depends on your case, for sure).
So it becomes handy to return the list of the actions to perform next, which can be empty, of course.
finiteStateMachine
:: forall action model
. action -> model -> model /\ List action
If you are still thinking about Elm, you may recall that Cmd.batch
thing, which represents effects that are evaluated from update
function where every effect is bound to the specific action. Let’s also join this concept with the specification of effects we did previously:
finiteStateMachine
:: forall action model
. action -> model -> model /\ Effect (List action)
It could be another way around:
finiteStateMachine
:: forall action model
. action -> model -> model /\ List (Effect action)
This way you can not share data between the effects you perform, only with carrying them together with actions. Which is really a good thing for making things really pure and minimalistic. But I decided to avoid that for the sake of simplicity, still you are always free to choose the proper way by just replacing one with another and make it compile.
I also made some helpers to refrain from typing pure
in the end of do
-blocks a lot. If you use List (Effect action)
technique, you don’t need them, obviously:
doNothing :: forall action. Effect (List action)
doNothing = pure []
single :: forall action. action -> Effect (List action)
single = pure <<< List.singleton
batch :: forall action. List action -> Effect (List action)
batch = pure
At this point, we are ready to define our data type:
data FSM action model =
FSM (action -> model -> model /\ Effect (List action))
Let’s also define the function to create such instances, which is just a type constructor for now, but we leave the opportunity for it to be changed later:
make
:: forall action model
. (action -> model -> model /\ Effect (List action))
-> FSM action model
make = FSM
That’s actually it! We can represent all other things just with reusing this one.
Errors, Introducing Covered
I have to confess that I am using my own data type to work with errors. As much of you probably did, I researched the problem of accumulating errors (no, I do swear I have no intention to have separate types for them!) and no solution satisfied me, except this one:
data Covered e a
= Carried a
| Recovered e a
Where Carried
means we carry the value and it’s all good. And Recovered
means we had some error and the value was recovered from that failure. It just always stores the last successful value, along with the error, when it had un-fortune to happen. The single and the latest error, same as for value,.. unless you have Semigroup e => Covered e a
!
Imagine you are cooking something and if you have forgot to buy some ingredient, you just skip it and happily continue the process, but notice such failure in the list of failures to avoid in the future. Or you broke the eggs together with the eggshells while preparing the cake, so you just suck the eggs and the eggshells back from the stock, and it’s all fine, you just put a notice near the cake that eggshells were there, and the eggs were also required to be reverted due to error, but it’s all clean now, bon appétit. Seems I am not that good in examples.
So the FSM, where it is possible to keep the last error (or accumulate errors), looks like this:
type CoveredFSM error action model =
FSM action (Covered error model)
But since we need to alter the update
function behaviour, and it should be imprinted in the logic of such machine, it could be better to make it a data type (and ensure to hide the constructor):
data CoveredFSM error action model =
CoveredFSM (FSM action (Covered error model))
Since Either
is the purest way of dealing with errors, I use it as an atomic part of the API: every function that belongs to the core API and can fail with some error, evaluates to Either
and nothing else. So does the update
function that wraps the API, except that inbetween the calls, after the action was applied to the model, it either (no pun intended) replaces Either
with the Covered
, holding the previous model (which we always have in update
function) or, if the error type satisfies Semigroup
typeclass, it glues the errors from the previous call with the ones from the ongoing call.
But first, let’s review the approach where we just store the last error and rollback to a last successful state. For that you’ll need a function that moves the error from one Covered
instance to another, if there is one. The function is simple:
consider
:: forall e a
. Covered e a -> Covered e a -> Covered e a
consider (Recovered errA _) (Carried vB) = Recovered errA vB
consider _ coveredB = coveredB
It seems to satisfy Alt
laws (but not Alternative
), so we may assign the <|>
operator for it. And this way you can use this function for folding the event streams of Covered
values, for example.
instance coveredAlt :: Alt (Covered e) where
alt = consider
And so we need to define custom make
function for CoveredFSM
:
make
:: forall error action model
. (action
-> Covered error model
-> Covered error model /\ Effect (List action))
-> CoveredFSM action model
make updateF =
CoveredFSM
$ FSM.make \action model ->
let model' /\ effects' = updateF action model
in (model <|> model') /\ effects'
If storing the last error satisfies your needs, you could stop at this point.
If you want to store all the errors happened, you’ll need some function to append errors from one Covered
instance to another. Let’s call it appendErrors
—it’s not Semigroup
’s append
, since it operates on errors rather than values:
appendErrors
:: forall e a b
. Semigroup e
=> Covered e a -> Covered e b -> Covered e b
appendErrors (Recovered errorsA _) (Recovered errorsB valB) =
Recovered (errorsA <> errorsB) valB
appendErrors (Recovered errorsA _) (Carried valB) =
Recovered errorsA valB
appendErrors (Carried _) coveredB =
coveredB
And it’s update function looks like this:
make'
:: forall error action model
. Semigroup error
=> (action
-> Covered error model
-> Covered error model /\ Effect (List action))
-> CoveredFSM action model
make' updateF =
CoveredFSM
$ FSM.make \action model ->
let model' /\ effects' = updateF action model
in (model `appendErrors` model') /\ effects'
A trained eye may notice a pattern here: we just have one function that changes the way how we join the previous model with the next one. If it sounds like folding to you, I share your inference. By defining a data type or newtype
we are ensuring that user uses the proper instance of CoveredFSM
and don’t forget to specify the way to glue errors. But that doesn’t prevent us from adding a helper to FSM
, such as:
joinWith
:: forall action model
. (model -> model -> model)
-> FSM action model
-> FSM action model
joinWith joinF (FSM updateF) =
FSM $ \action model ->
let model' /\ effects' = updateF action model
in (model `joinF` model') /\ effects'
And now our make
functions become much fancier:
make
:: forall error action model
. (action
-> Covered error model
-> Covered error model /\ Effect (List action))
-> CoveredFSM error action model
make =
FSM.make
>>> FSM.joinWith ((<|>))
>>> CoveredFSM
make'
:: forall error action model
. Semigroup error
=> (action
-> Covered error model
-> Covered error model /\ Effect (List action))
-> CoveredFSM error action model
make' =
FSM.make
>>> FSM.joinWith Covered.appendErrors
>>> CoveredFSM
One of the downsides of using Covered
could be that it’s not that fancy to useCovered
in do
-notation, rather than its brothers Maybe
and Either
since (unless you wrap the Either
-producing function in Covered
later, as noted above) you always have to specify the fallback value and it is usually the same value through all the block. I suppose it could potentially be solved with monad transformers and State
monad, and if yes, please tell in the comments how.
FRP and Running
Finally, let’s implement it! Using event streams from the FRP Events Library.
It’s rather simple: we provide the initial model (init
), we create the actions stream, and on every push of some action, we call the FSM’s update
function on it, skipping the effects from previous update. And we subscribe to the stream of updates to perform all the effects requested after the update. Then we provide user with the ability to push
actions into system. Which is quite useful for UIs for example, to push some specific action in response to the HTML event handler.
run
:: forall action model
. FSM action model
-> model
-> Effect
{ push :: action -> Effect Unit
, stop :: Effect Unit
}
run (FSM updateF) init = do
{ event : actions, push } <- Event.create
let
(updates :: Event (model /\ Effect (List action))) =
Event.fold
(\action prev -> updateF action $ Tuple.fst prev)
actions
(init /\ pure [])
stop <- Event.subscribe updates
\(_ /\ eff) -> eff >>= traverse_ pushAction
pure { push, stop }
This has a little sense though, since you have no way to see what models are, so let’s add the ability to specify the subscription to models. The problem with just returning the event stream of models is that if you subscribe to it after the subscription which performs the effects, you get the results of these calls in the model stream as well, which you would probably like to avoid.
run
:: forall action model
. FSM action model
-> (model -> Effect Unit)
-> model
-> Effect
{ push :: action -> Effect Unit
, stop :: Effect Unit
}
run (FSM updateF) subModels init = do
{ event : actions, push : pushAction } <- Event.create
let
(updates :: Event (model /\ Effect (List action))) =
Event.fold
(\action prev -> updateF action $ Tuple.fst prev)
actions
(init /\ pure [])
(models :: Event model)
= Tuple.fst <$> updates
stopModelSubscription <- Event.subscribe models subModels
stopPerformingEffects <- Event.subscribe updates
\(_ /\ eff) -> eff >>= traverse_ pushAction
pure
{ push : pushAction
, stop : stopModelSubscription <> stopPerformingEffects
}
We can use it to fold some list of actions and get the latest model out of it:
fold
:: forall action model f
. Foldable f
=> FSM action model
-> model
-> f action
-> Effect model
fold fsm init actionList = do
lastValRef <- Ref.new init
{ pushAction, stop } <-
FSM.run fsm (flip Ref.write lastValRef) init
_ <- traverse_ pushAction actionList
lastVal <- Ref.read lastValRef
pure lastVal
Running a CoveredFSM
instance is just calling the FSM.run
for the underlying instance, like this:
run
:: forall action model
. CoveredFSM action model
-> (model -> Effect Unit)
-> model
-> Effect
{ push :: action -> Effect Unit
, stop :: Effect Unit
}
run (CoveredFSM fsm) = FSM.run fsm
UI, Renderers and VDOM
Now, to the UI part. Finite State Machine only lacks one addition to be able to render model into some view. And this addition is easily represented with a corresponding function:
data UI action model view =
UI (FSM action model) (model -> view)
The UI which stores the information about errors is:
type CoveredUI error action model view =
UI action (Covered error model) view
Since we hide the FSM
under the UI
type constructor, we may avoid using CoveredFSM
type and provide making functions like these:
make
:: forall action model view
. (action -> model -> model /\ Effect (List action))
-> (model -> view)
-> UI action model view
make updateF viewF =
UI (FSM.make updateF) viewF
makeCovered
:: forall error action model view
. (action
-> Covered error model
-> Covered error model /\ Effect (List action))
-> (Covered error model -> view)
-> UI error action model view
makeCovered updateF viewF =
UI (FSM.make updateF # FSM.joinWith (<|>)) viewF
makeCovered'
:: forall error action model view
. Semigroup error
=> (action
-> Covered error model
-> Covered error model /\ Effect (List action))
-> (Covered error model -> view)
-> UI error action model view
makeCovered' updateF viewF =
UI (FSM.make updateF
# FSM.joinWith Covered.appendErrors) viewF
And, this way running UI is as easy as:
run
:: forall action model view
. UI action model view
-> model
-> Effect
{ next :: Event view
, push :: action -> Effect Unit
, stop :: Canceler
}
run (UI fsm viewF) model = do
{ event : views, push : pushView } <- Event.create
{ push, stop } <-
FSM.run fsm (pushView <<< viewF) (Covered.carry model)
pure
{ next : views
, push
, stop
}
What user gets is response is the stream of views and we can now feed it to the rendering engine.
Let’s address to Halogen
VDOM engine which is distributed in a separate package. First, we now definitely render to HTML:
type HtmlRenderer error action model =
CoveredUI error action model (Html action)
Another confession I have to make: currently, yes, it’s the Html
from the Spork
library. But since in this article we intentionally decline the techniques behind the libraries like Spork
for the sake of learning, I had to keep it in secret till the end. Also, it is still up to you which output you want to have, SVG or canvas or text string or may be even you will decide to output to terminal using ASCII, for all view
s it works the same!
And we’re just giving the specific examples.
This code is a bit more complicated since VDOM
and HTML
API are both not as friendly as ours, but still it works like a charm:
embed
:: forall action model
. String
-> HtmlRenderer action model -- renderer
-> model -- initial model
-> Effect Unit
embed sel render firstModel = do
doc <- DOM.window >>= DOM.document
mbEl <- DOM.querySelector
(wrap sel)
(HTMLDocument.toParentNode doc)
case mbEl of
Nothing -> throwException
(error $ "Element does not exist: " <> sel)
Just el -> do
{ next, push }
<- UI.run renderer firstModel
let
vdomSpec = V.VDomSpec
{ document : HTMLDocument.toDocument doc
, buildWidget: buildThunk unwrap
, buildAttributes: P.buildProp push
}
first_vdom <- EFn.runEffectFn1
(V.buildVDom vdomSpec)
(unwrap
$ UI.view renderer
$ Covered.carry firstModel)
vdom_ref <- Ref.new first_vdom
void $ DOM.appendChild
(Machine.extract first_vdom)
(DOMElement.toNode el)
cancel <- Event.subscribe next $
\next_view -> do
prev_vdom <- Ref.read vdom_ref
next_vdom <- EFn.runEffectFn2
Machine.step
prev_vdom
(unwrap next_view)
_ <- Ref.write next_vdom vdom_ref
pure unit
pure unit
Wait… It turns out the VDOM
engine uses the Finite State Machines under the hood as well. Just a bit more complicated ones.
The Stub and the Actual App
We need some actual code to work with the system, let’s do some stubs:
data Error = Error
data Action = Action
data Model = Model
init :: Model
init = Model
update
:: Action
-> Covered Error Model
-> Covered Error Model /\ List (Effect Action)
update action covered = covered /\ List.Nil
view
:: Covered Error Model
-> Html Action
view _ =
H.div [] [ H.text "example" ]
myRenderer :: HtmlRenderer Error Action Model
myRenderer =
Ui.makeCovered update view
Finally, your main function can now be as easy as:
main :: Effect Unit
main =
VDom.embed "#app" myRenderer init
That’s it, folks!
Aftermath
Aftermath One
What we defined as UI
is actually containing both application logic (the FSM
stored inside) and rendering (model -> view
function), so you could want to separate these functions or just rename UI
to App
and abstract your application by view
:
data App action model view =
App (FSM action model) (model -> view)
data App' error action model view =
App'
(FSM action (Covered error model))
(Covered error model -> view)
type MyApp view = App' Action Model view
myApp :: App Action Model (Html Action)
myApp = Ui.makeCovered update view
It is the same things as HtmlRenderer
defined above, so embedding is no different:
main :: Effect Unit
main =
VDom.embed "#app" myApp init
But now you may reuse the same logic for different views.
Aftermath Two
Another thing. This would be useful to map
over the FSM
types to convert, for example, FSM action (Either error model)
to FSM action (Covered error model)
with just one call, but if you try to implement Functor
instance for it, you’ll find that to do it we also need a function to convert Covered
to Either
back, which breaks the Functor
logic, of course. But it looks like there’s Invariant
for that!
imapModel
:: forall action modelA modelB
. (modelA -> modelB)
-> (modelB -> modelA)
-> FSM action modelA
-> FSM action modelB
imapModel mapAToB mapBToA (Fsm updateF) =
FSM \action modelB ->
Bifunctor.bimap mapAToB identity
$ updateF action
$ mapBToA modelB
instance invariantFSM :: Invariant (FSM action) where
imap = imapModel
…Unfortunately, no, imap
is not enough, because you can not create Covered
out of thin air if there’s Left error
value in Either
part—you need some value to put in Covered
. You may use imap
for any cases where models are easily converted both one to another and back without the loss of data.
Aftermath Three
Remember I noticed that it’s better to use List (Effect action)
rather than Effect (List action)
? It indeed is, and it requires just one change in the code of the run
function. To replace:
stopPerformingEffects <- Event.subscribe updates
\(_ /\ eff) -> eff >>= traverse_ pushAction
with
stopPerformingEffects <- Event.subscribe updates
\(_ /\ effs) -> traverse_ ((=<<) pushAction) effs
Done. The example is written using List (Effect Action)
, by the way.
Aftermath Four
And the last. Covered
type has the Bind
instance such as (where recover
extracts the value from the Covered
type):
instance coveredBind
:: Semigroup e => Bind (Covered e) where
bind covered k = appendErrors covered $ k $ recover covered
You’ve seen appendErrors
above. So now you know, that you may use >>=
anywhere to join errors between two Covered
values in any place.
Aftermath Five
One project that inspired me for using PureScript with FRP is Flare. The way it uses Functor and Applicative instances to adapt the values inside of the components is just awesome and they are all just one-liners. So the future plan is to find a way to do similar things with FSMs.
And Everything Else…
Don’t forget to take a look at the example which has the code with the effects, and passing actions from the UI
and everything you would question about during reading this article. Hope you enjoyed it.
Also, here is the example source code.
On the other hand, the article could contain errors and misleading information, not intentionally, of course. If you notice such case, please inform the author and the readers as soon as possible by leaving a friendly, yet correcting, comment.
If you see the ways to improve the approach, please also do comment. Even comment if you have ideas on how to do things worse.
-
There is some controversy on the effectiveness of the approach, but let’s decide the author (for sure) and the reader (hope so) still think the approach is just awesome, if you use it right. ↩
Top comments (4)
This is a very informative and well written article, thank you!
A follow-up question. You say:
But why is this the smart thing to do here? What do can we get from it, what benefits will it bring compared to simply returning "Effect model"? Will it help in testing/mocking somehow or are there any other gains?
I hope a late reply still counts.
Effect model
in contrast toEffect (List action)
is completely hiding the things one does with the model during theEffect
process, so it is much harder to tell and debug what happened there. So yes, it will help in testing as well. When the list of actions to perform is returned and theupdate
is granular or atomic, so any action makes only a subtle indivisible change to a model, then anything what happened is trackable.This concept was also praised in Redux and with the
useEffect
in React — it is easier to track actions if they are as minimal, indivisible and descriptive as possible.Plus, it gives you the way to rollback the effects one by one and implement the Redo/Undo in just few easy steps.
Thank you!