Where does logic live?

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 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 Intent in 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 .. else branching).
  • I’m not satisfied by the usage of imperative language for both Fx and Intent 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 opaque Cmd being produced
  • It might be overly complicated :slight_smile:
  • In my ear Fx and Fact 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 :innocent: (shameless plug ™)

1 Like