TL;DR
Sorry this turned out to be a lengthy one… The gist of it is: what about using separate types for messages that represent Fact
s and Intent
s instead of lumping it all into Msg
and rely on convention and additional tests? If you’d like to know more have a and proceed.
Edit: sorry the intial post had some huge copy&paste problems
The Initial Spark
I recently had a chat on the Elm slack on how to make the Cmd msg
s of your update
results better testable. It turned out that a number of people have kind of the same ideas floating about their heads. The initial motivation for this is that once created we can’t inspect Cmd
s in Elm. Abstracting over the thing-to-do, in a way that is easily testable, would be to represent the Effect
that the Cmd
should achieve with a union type.
Here is a working Ellie of this initial approach that sports some helper code to adapt our new update
function to the usual TEA layout, this can also be found on github.
-- (A) Before: one message type
type Msg
= WhoIsTheKingInTheNorth
| GotAnswer (Result Http.Error Character)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
WhoIsTheKingInTheNorth ->
let
request =
Http.get
"https://anapioficeandfire.com/api/characters/583"
decodeCharacter
in
( model, Http.send GotAnswer request )
-- (B) After: a message type and an `Fx` type
type Msg
= WhoIsTheKingInTheNorth
| GotAnswer (Result Http.Error Character)
type Fx = FetchJonSnow
update : Msg -> Model -> ( Model, List Fx )
update msg model =
case msg of
WhoIsTheKingInTheNorth ->
-- This update result can easily be verified with a test
( model, [ FetchJonSnow ] )
-- ...
evalFx : Fx -> Model -> ( Model, List (Cmd Msg) )
evalFx fx model =
case fx of
FetchJonSnow ->
let
request =
Http.get
"https://anapioficeandfire.com/api/characters/583"
decodeCharacter
in
( model, [ Http.send GotAnswer request ] )
Note that the update
function is now fully testable, so the intention of the-thing-to-do can be verified and supplied with data through an appropriate constructor. I wasn’t sure whether to go with the List (Cmd Msg)
or just a plain Cmd Msg
, which are pretty much both not really inspectable so using Cmd.batch
would probably do no harm.
The whole exercise was so much fun that I continued to ponder what more we could do with this.
Voices in my head
While thinking about the nice improvement from (A)
to (B)
I became aware that the same approach could help to resolve another issue I’ve been thinking about lately. It always bothered me that most message based systems treat all messages equally when in reality more often than not they serve different purposes. In event-sourced systems you may involve Command-Query-Responsibility-Segregation (CQRS) meaning that you have commands and queries that you treat very distinctly although both are message types:
- commands are messages the receiver can refuse to handle due to validation or other business assessments, this is how users/systems initiate a change of state in your application
- while an event represents something that happened in your system in the past so it doesn’t make sense that you could even have the option to refuse to handle them, once stated it is immutable. It also doesn’t make sense that events could be introduced from the outside without a command, you want to protect your invariants, don’t you? The single source of truth in such a system is the eventlog - your current state at a specific point in time is a left-fold of all the events up until that point.
The Elm Architecture is one of the event-sourced systems that has only one message type, usually called Msg
, which is nice from a usability perspective but the type system can not prevent you from handling messages in the wrong way, it’s all just convention and tests you need to know about/remember to write.
To illustrate the issue I see, let’s have a look at the update
function which contains a mix of responsibilities, it handles:
- messages that the user produces by interacting with the UI (i.e. through
Html msg
), I like to call themIntent
s which reads nice and serves to avoid the overloaded term command. - messages that an outside system has introduced via
Sub
s - likeMouse.downs
- which I would also categorize as anIntent
to change the state of the system which needs to be checked by the system. - messages that are the outcome of some
Cmd
which typically represent the equivalent of events in CQRS. I like to call theseFact
s, again for readability and disambiguation.
I also find it strange that a view
in TEA is allowed to produce any kind of message, if it type checks it’s OK, right?
-- (C) Did you mean to check first?
type Msg
= CheckForRocketLaunches
| SomebodyElseShotFirstLetsRetaliateImmedialtely
view _ =
button
[ onClick SomebodyElseShotFirstLetsRetaliateImmedialtely ] -- Oops
[ text "Check for rocket launches around the world" ]
In light of these considerations I propose a more structured way to think about handling messages.
Intents and Facts
As I see it Fact
s are the only kind of messages that would go into your immutable eventlog and therefore would make up your state. Sometimes it is useful to also store the Intent
s of the users but to recreate your current state from scratch, a.k.a. the Model
in Elm, a left-fold of your recorded facts is what you usually want. This has the huge benefit of being able to replay your events from scratch without the need to concern yourself with side-effects that might be introduced, they’re just facts after all. Intent
s serve the purpose of evolving your state in a more controllable way. By using them as the only means to let information flow into the system we could let the compiler handle a whole category of bugs we have to write tests for right now. Making impossible states even more unrepresentable . I tried to make Elm’s type system a more useful ally in segregating intents and facts so I came up with some helpers that make representing the appropriate distinction in types.
Here a Program
wouldn’t handle generic Msg
s but more specifically Intent
s to advance the state of the system. A working version can be found in this Ellie and this github gist.
-- (D) A bird's eye view on what we're doing
apply : Fact -> Model -> Model
interpret : Intent -> Model -> ( List Fact, List Effect )
produce : Effect -> Model -> Cmd Fact
main : Program Value Model Intent
main =
Html.programWithFlags <|
wrap
{ apply = apply
, init = init
, interpret = interpret
, join = StateFact -- Needed to turn `Cmd Fact` into `Cmd Intent` for TEA
, produce = produce
, subscriptions = subscriptions
, view = view
}
type Intent
= AskWhoIsTheKingInTheNorth
| Incoming Value
| StateFact Fact -- Really not happy about this, but seems necessary
type Fact = GotAnswer (Result Http.Error Character)
type Effect = GoFetchJonSnow
To initialize the system as with TEA you would need a default state and, going with the initial idea of separating Cmd
s from your normal update
logic, a list of effects you want the Elm runtime to perform. I also found that having all the fact-handling in one place is nice so maybe we produce a list of facts we want to have applied to the initial state instead of doing it directly in the init
function.
-- (E) Initializing our state
init : Value -> ( Model, List Fact, List Effect )
init flags =
( {}, [], [] )
Setting up subscriptions you might notice that messages from the outside will only be able go through our validation logic by producing intents (†).
-- (F) Safer subscriptions
subscriptions : Model -> Sub Intent
subscriptions model =
Sub.batch
[ toElm Incoming
]
Handling facts would be akin to what Html.beginnerProgram
does. Note that in this iteration this is a dead-end in terms of control-flow, see trade-offs later on.
-- (G) Easier handling of plain facts
apply : Fact -> Model -> Model
-- Alternatively...? apply : Fact -> Model -> ( Model, List Intent )
apply fact model =
case fact of
GotAnswer (Ok { name, titles }) ->
Debug.log (name ++ "/" ++ String.join "," titles)
model
GotAnswer (Err reason) ->
Debug.log ("Couldn't find the answer: " ++ toString reason)
model
Validating user/outside system intents would be the concern of another function. This would be the place to validate and refuse an intent if necessary by just returning ( [], [] )
. Note also that if there wasn’t a technical need for the StateFact Fact
intent the user/outside systems wouldn’t have the ability to just blindly state facts in your system without you validating their intents (†). This could be useful in filtering out unwanted Sub msg
s coming from Mouse.moves
and the like and you wouldn’t need to rely on convention to do this here because the types dictate that you can only do it here.
-- (H) All validation logic needs to go here and is testable
interpret : Intent -> Model -> ( List Fact, List Effect )
interpret intent model =
case intent of
AskWhoIsTheKingInTheNorth ->
( [], [ GoFetchJonSnow ] )
Incoming json ->
( [], [] )
-- Why won't you let me be?
StateFact fact ->
( [ fact ], [] )
To produce an actual Cmd
for the Elm runtime to run we would have another function that is only concerned with turning a user defined effect into what it actually amounts to in terms of TEA. This way we have pushed the not so testable code outside of our business logic, again without the need to rely on conventions. (I’m not sure that this is really necessary but for the sake of argument I’ll just include it)
-- (I) Finally producing a `Cmd` for the Elm runtime
produce : Effect -> Model -> Cmd Fact
produce fx model =
case fx of
GoFetchJonSnow ->
let
request =
Http.get
"https://anapioficeandfire.com/api/characters/583"
decodeCharacter
in
Http.send GotAnswer request
To complete the picture we have our view function that, interestingly enough, is now only allowed to produce intents, so the compiler would not let you just state facts from the user interface alone. Which might seem tedious for simple examples but I think that might be a good trade-off to make (†).
-- (J) Safer views
view : Model -> Html Intent
view model =
Html.button
[ onClick AskWhoIsTheKingInTheNorth
]
[ Html.text "The King in the North!"
]
The Elephant in the Room
(†): The StateFact Fact
constructor of Intent
is a wart I wasn’t able to get around since you need a way to channel the Cmd Fact
s back into TEA via Cmd.map
. Maybe the signature could be produce : Effect -> Model -> Cmd Intent
but then i.e. GotAnswer
would also need to be turned into an Intent
which doesn’t seem right to me. Using the dreaded StateFact Fact
escape hatch could alleviate the problem. Doing it this way would free us from having to cope with the control-flow-dead-end in (G)
, maybe it’s worth the effort?
-- (K) Potential alternative to returning a `Cmd Fact` and having to `Cmd.map`
produce : Effect -> Model -> Cmd Intent
produce fx model =
case fx of
GoFetchJonSnow ->
let
request =
Http.get
"https://anapioficeandfire.com/api/characters/583"
decodeCharacter
in
Http.send (\it -> StateFact (GotAnswer it)) request
Discussion
This has some very visible trade-offs:
- There is obviously more stuff to set up and state handling would be fanned out to more functions than just
update
which might get unwieldy? To me it seems that the clearer separation would outweigh more complexity in bigger projects. - Except for intents and facts the terms are in a fluid state for me right now, so if you have any suggestions I’d love to hear about them!
- I don’t like the term
Effect
- it looks and sounds too similar toFact
and is too generic to my mind - but I couldn’t find a better one: Ambition, Challenge, Consequence, Endeavor, Motivation, Procedure, Quest, Saga, SideEffect, Venture? MaybeConsequence
would be a good replacement? - I’m currently not sure how you would model the “fetch something and then do this” workflow without the ability to sequence
Cmd Fact
s chronologically, whichCmd.batch
doesn’t guarantee AFAIK, changing theapply : Fact -> Model -> Model
signature toapply : Fact -> Model -> ( Model, List Intent )
, which would make applying Facts a little more awkward or producingCmd Intent
the way it is done in(K)
. Any suggestions? - Maybe having the need for a separate union type for describing the effect of a
Cmd
isn’t that necessary after all, I still like that it is more testable but I could imagine purging that to reduce the complexity overall.
Conclusion
The whole idea might just be a bad case of over-engineering since you can always separate functionality out into helper functions but my feeling is that having distinct types for messages that need to be treated differently from each other might be worth thinking about .
I’m interested in what you think about this and how one could improve upon it? If you’ve come this far you really deserve another or a