I’d like to propose a variant on “option 1 - let the update function figure it out” I’ve been exploring for a while now (I’ve omitted the plumbing).
type Msg = Fact | Intent
type Fact = RocketLaunchRequested | LaunchPrevented
type Intent = LaunchRockets
type Fx = DearElmRuntimePleaseLaunchRockets | GoInvestigate
apply : Fact -> Model -> Model
produce : Fx -> Model -> ( Model, Cmd Msg)
interpret : Intent -> Model -> ( List Fact, List Fx )
interpret intent { userName } =
case intent of
LaunchRockets ->
-- Your projection from the model is done here
if userName == "Dr Evil" then
-- Hmmmmmm, no!
( [ LaunchPrevented ], [ GoInvestigate ] )
else
( [ RocketLaunchRequested ], [ DearElmRuntimePleaseLaunchRockets ] )
view : Model -> Html Intent
view model =
Html.div [ onClick LaunchRockets ] [ Html.text "Launch!" ]
-- Can't produce unchecked facts! This won't even compile
-- Html.div [ onClick RocketLaunchRequested ] [ Html.text "Launch!" ]
Pros:
- I very much like that my views can’t produce the wrong kind of message and that all unchecked intents need to actively be validated.
- It’s also easy to see what the actual intention of some user action is without the implementation noise in between when looking at the
interpret
function. -
interpret : Intent -> Model -> ( List Fact, List Fx )
is possible/easy to test and is probably readable as is for a domain expert who isn’t a programmer. Bonus: this contains all the hard business logic whereas the rest of the code is mostly implementation details. -
produce : Fx -> Model -> ( Model, Cmd Msg )
still retains all the power of a “normal” update function but is pushed into the “last resort” phase talking to the runtime. - The
apply : Fact -> Model -> Model
logic is much simpler and more focused. - Because
Fact
s have to be named explicitly I think harder on the naming and if it’s actually supposed to do what I thought in the first place. - You can easily collocate the
Intent
in a separate module concerned only with the view. - It’s also possible to have
Sub Intent
s for subscriptions from ports you may want to validate semantically and in context before letting them into your system.
As of now there are some caveats to this that are bothering me:
- I’m not really sure how to handle showing/hiding stuff without resorting to a projection for the view like @ianmackenzie mentioned (or
if .. then .. else
branching). - I’m not satisfied by the usage of imperative language for both
Fx
andIntent
so you can’t tell them apart easily, haven’t figured that one out yet, maybe they are one and the same? - Adding stuff tends to get noisy because you usually have 1-to-N intents to facts and 1-to-M intents to effects
-
produce
isn’t readily testable due to the opaqueCmd
being produced - It might be overly complicated
- In my ear
Fx
andFact
sound too similar when spoken out loud
If you’re interested in how I got to this monstrosity, there is an older discourse thread about it (shameless plug ™)