Removing "NoOp" messages with dummy events

You sometimes want to “ignore” an event based on application state. For example, let’s say that clicking on a user’s profile picture takes you to their profile. But if a user has left the platform, you might want their posts to keep existing with their profile picture, but nothing should happen when you click on their portrait picture.

A common pattern to handle this in Elm applications (according to a quick search with grep.app) is by using a “dummy” NoOp message which, when handled in update, just returns the Model. In the HTML attribute, you write code that looks like this:

if someCheckPassed then
onClick DoSomething
else
onClick NoOp

Both branches have to generate the same type, so you can’t leave one of the blank. The downside is that this clutters up your Msg type, is (minor) noise in your update function, and is also noise in the debugger.

So I thought, why not just ignore it at the source? One can write:

if someCheckPassed then
onClick DoSomething
else
on “dummy” (Json.Decode.fail “nope”)

Now that event just gets silently discarded at the source; the update shouldn’t even trigger for it. No extra message, no debugging noise. It might even be cheaper than a NoOp message, unless I’m missing a trick. You can also encode this logic into a tiny function so that one can write

someCheckPassed |> thenPermitInteraction (onClick DoSomething)

(Another way around is to supply some non-event Attribute in the else branch instead of an always-failing event, but that might add noise to the HTML.)

I’m probably not the first to think of this approach. It seems like a cleaner solution than NoOp messages. Is there some drawback to it that I’m not seeing?

Assuming the onClick event is added to a List Html.Attribute, you can also only add it to the list only if someCheckPassed is True:

if someCheckPassed then
    [ onClick DoSomething ]
else
    []
2 Likes

Nice! I like to emit the message anyway, as it represents the fact that SomethingHappened.

Then, no matter how many places can allow Something to Happen, the only place I have to consider what do in response to that happening, is update.

This comes up often where there are keyboard shortcuts for things that are also triggerable thru clicks.

1 Like

I think the approach that @rkb outlines is the real winner here. Messages should be events (UserClickedLoginButton), not commands (`ShowLoginForm`). With this NoOp is never needed, and all the business logic stays in update, rather than being split across view and update.

3 Likes

Plausible. But this turns update into a very micro-level affair which can, at scale, bury the actual business logic and make the Message type itself unwieldy. Let’s say that you have a few different “Settings” pages, for example; now, do you have UserSettingsApplyClick, EditorSettingsApplyClick, SharingApplyClick, etc messages? Or do you have a single ApplyButtonClick message, and then you use some logic in update to figure out what’s going on based on the Model?

I’m not saying it’s the wrong approach at all or even in most cases, but I’ve run into cases where a 200-case Message type would probably be a really bad idea from the viewpoint of understanding what the application logic actually is.

As @lpil said, Msg should not reflect what happens in the business logic, it should reflect what happened. And it should contain enough information to indicate what’s going on without looking at the contents of the Model. Let’s say you have 2 similar buttons, you could do either of the following options.

type Msg
  = UserClickedSaveButton
  | UserClickedCancelButton

-- or

type ButtonKind
  = SaveButton
  | CancelButton

type Msg
  = UserClickedButton ButtonKind

In practice, people would mainly go for the first option. The important part is that you’re able to know as much about what happened as what is required by your business logic (in other words, your update branch).

If you want to know more about this way of doing/naming things, check out this talk: Message Naming Conventions - Noah Zachary Gordon

do you have a single ApplyButtonClick message, and then you use some logic in update to figure out what’s going on based on the Model?

Going with an approach like the following will be error-prone because you will have to ensure the logic in update and in view match, and will sometimes be indiscernible from logic because both buttons show up at the same time.

type Msg
  = UserClickedButton

update msg model =
  case msg of
    UserClickedButton ->
      if showsSaveButtonInView model then ... else ...

Let’s say that you have a few different “Settings” pages, for example; now, do you have UserSettingsApplyClick, EditorSettingsApplyClick, SharingApplyClick, etc messages?

I seem to understand that you have a single Msg type in your application (which explains why you have 200 of those). What is usually done is to split your application up into multiple Elm modules that may have their own Msg, update and view.

So you could have in your Main file:

type Msg
  = UserSettingsPageMsg UserSettingsPage.Msg
  | EditorSettingsPageMsg EditorSettingsPage.Msg
  | ...

update msg model =
  case msg of
    UserSettingsPageMsg subMsg ->
      -- Naive way of storing page info in the Model, adapt to your needs
      { model | userSettingsPage = UserSettingsPage.update subMsg model.userSettingsPage }

    EditorSettingsPageMsg subMsg ->
      { model | editorSettingsPage = EditorSettingsPage.update subMsg model.editorSettingsPage }

and then in those modules you would have Msgs that relate to things that can happen in the view of that module.

type Msg
  = UserClickedSaveButton
  | UserClickedCancelButton

I think that, on a philosophical level, I disagree with @lpil’s approach—but I think that it is a good starting point for interaction design. If there are three ways of accessing the same functionality—say, by clicking on a button, or by swiping, or by using a keyboard shortcut—then it makes little sense to me to always have three separate messages that all lead to the same function. So I can start with that sort of event-driven messaging; but once I understand my app, why would I want to reconstruct the semantics of what the user wants to do, when the message itself can describe it? (the talk that @jfmengels pointed me to makes the same point for mature applications).

I also think it’s important to mention, for other readers, something that has yet to be said: a view is typically evaluated many more times than an update is, and this is precisely because update is essentially gated by the permitted messages. If only for performance, it always makes sense to keep computation in the update, and serve only (cached) values in the view.

But the intersection between update and view is the event itself, and I cannot see why one would regard the firing (or not) of an event, based on the Model, as business logic; and that seems to be what some in this thread have understood it to be, unless I am misunderstanding. It’s certainly true that having multiple paths in one’s view (e.g. excluding/including certain elements depending on the Model) is not considered to be business logic. (Let (s)he who has never had multiple paths in their views cast the first stone… and also reveal how on earth that feat was accomplished!). It seems odd to say that one can use the Model to exclude an entire element—including its events—and that is not business logic, but including the element without its events is business logic.

Is there some deeper reasoning behind this that I’m not seeing?

There’s nothing about using an event based message design that means you need have a multiple variants for each thing, in the way you have described. The only reason you would split up a variant is if you care specifically that difference in your business logic.

My event based applications tend to have fewer variants as the business meaning of the event can be contextual, rather than a more specific command.

In the specific case of clicks you can put the test on a disabled attribute instead

1 Like

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