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
interpretfunction. -
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 -> Modellogic is much simpler and more focused. - Because
Facts 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
Intentin a separate module concerned only with the view. - It’s also possible to have
Sub Intents 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 .. elsebranching). - I’m not satisfied by the usage of imperative language for both
FxandIntentso 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
-
produceisn’t readily testable due to the opaqueCmdbeing produced - It might be overly complicated
- In my ear
FxandFactsound 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 ™)