Where does logic live?

I’m trying to figure out what kind of logic should run during view and what should run during update.

Suppose I have a button whose operation depends on the current context. (e.g. the first time you press it it triggers a SaveValue, and following it’ll trigger an UpdateValue).

There are 2 ways I can do this:

  1. have view trigger a generic ButtonClicked message, and sort out the operation based on the model during update.
  2. do the work to sort out the operation based on the model during view, and have the button trigger the specific message.

The immediate intuition may be that the first is better. That is, do your real work/logic in update. But, consider functions like preventDefaultOn and stopPropagationOn. They require us to make this sort of “model -> behaviour” calculation in view.

The only other option is to calculate shouldPreventDefaultOn... in update, and use that pre-computed value during view, but this goes against the idea of storing derivable values.

I’m left to conclude, that it is most consistent (and correct), for view to determine the exact nature of the message to be dispatched. So we should never see messages like KeyPressed Int or ButtonPressed, but rather ScrollDown and SaveValue (for example).

Thoughts?

2 Likes

I favor 2 heavily. I think it embraces the “UI is a function of application state” philosophy, while 1 works against it.

Your last sentence really resonates with me, but I think I take it even further. If I can help it at all, my update functions don’t even know they’re driving a webapp. With the right wiring you could theoretically use them in a CLI app, for example. Sometimes you’ve gotta be tracking mouse positions and other browser-ey things, but you can still get away with framing messages more in terms of abstract, “business logic” level actions to a considerable degree.

I feel like this helps keep my update functions simple, and makes UI changes way easier, because no stale details of the UI design are lurking around in update. Downside is that it takes a bit more time and effort to write down Model and Msg types.

6 Likes

If some information can be “derived” from the Model, I recommend not putting it in the Model.

If you were to store information in your model that can be “derived” from other information, you are creating a “duplicate” entry of sorts. For example, if you were to store items : List Item and numItems : Int, you now have data in your model that can get out of sync with other data. Maybe your colleague remove from items but does not know to update numItems. So this can be a subtle way of undermining the “single source of truth” defense against synchronization bugs. Running List.length is not expensive enough for me to open myself up to that kind of bug.

So with anything that can be derived, I try to do it in helper functions. If the derived information happens to be needed in view, I might hide the computation behind a lazy if I am worried about perf. For example, I prefer to have an “expensive” sort in my view than to have two lists to synchronize insertions and deletions in my model. (I put “expensive” in quotes because sorting lists probably will not even register compared to DOM operations.)

7 Likes

One nice-looking approach that seems like it might help bridge the gap is having a “view model” as described in this NoRedInk blog post by @stoeffel and @jwoudenberg. The basic idea is that you have

  • update as normal, don’t compute/store any derived data
  • toView : Model -> View which computes any derived data from Model needed in view and stores it in a separate View type
  • toHtml : View -> Html Msg which should have very little real logic, mostly just a direct translation of the View type to Html
  • view = toView >> toHtml

This means that

  • update can work purely on business logic, doesn’t have to compute any derived values since that will be done in toView
  • toView acts as a bridge between business logic and view logic
  • toHtml can be a pure ‘rendering engine’ only concerned about layout/styling, with any significant computations already having been performed for it in toView
  • testing of toView is easier than a classic view function because you can write tests for the View type (which can easily be decomposed into UserView, CommentView, LoginFormView etc. types) more easily than for Html

I confess I haven’t yet tried this myself, but I’d like to - in your example it seems that you could do things like figure out which message to send from the button in toView and store that derived info in the View type so that toHtml itself can be concerned purely with generating HTML.

7 Likes

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

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.