Why not use: update fn state = fn state

Hi Everyone

I’ve heard about Elm for a long time, particularly from @jxxcarlson. I’ve now starting learning and coding so that I can write an app. And I’ve a beginners question.

In most Elm apps, the update method is quite large, with a large case statement based on the msg sent to update. That frightened me. So I wondered, perhaps we could simplify things by writing

update fn state = fn state

The Elm Guide has a simple incr / decr example. I’ve rewritten it using this very simple update function (see below). My question is this. Are there any problems in taking this approach?

module Main exposing (main)

import Browser
import Html exposing (button, div, text)
import Html.Events exposing (onClick)

main =
  Browser.sandbox { init = init, update = update, view = view }

init = 0

update fn state = fn state

incr n = n + 1
decr n = n - 1

view state =
  div []
    [ button [ onClick incr ] [ text "+1" ]
    , div [] [ text (String.fromInt state) ]
    , button [ onClick decr ] [ text "-1" ]
    ]

To my beginner’s mind, I find the above easier to read and understand. As part of my learning curve, I’ve also put this code up at https://ellie-app.com/cwR5cq2RH2ya1.

5 Likes

Messages shouldn’t contain functions because they need to be serialized, otherwise you won’t be able to use elm time travel debugger.
Personally I prefer to write long case expressions, because in a bigger case with 20+ branches, I’d need to create and call correctly all the 20 functions, but it’s a personal preference.

Hi @jfine2358 welcome!

You can definitely put functions in your messages but there are several things that you have to be careful with when doing that. First you cannot see messages that flow through your app. If you click on the DEBUG tab after clicking a bit on buttons of that ellie you give, you’ll see that messages are represented by <internals>. That’s less helpful. You will also not be able to import that trace into the debugger to replay it. (Try exporting and then importing it, you’ll get an exception).

But for many people, the debugger isn’t really a problem. Yet there are still advantages in not using functions in messages. In particular, when apps tend to grow, it is often better to describe what event happened (plusButtonClicked) than to give an intent (increment), and to transform that event into an intent in the update function. This makes it clear that the only place where logic happens in your whole application is your update function and the functions it calls. The view is just pure data visualization in the form of HTML.

4 Likes

This is a very interesting approach and the closest thing to a solution to the issue of bloated update functions I’ve witnessed until now!

I like that the Elm community considers the forces involved when it makes a design decision. Functions or messages in update depends on the forces. When I posted, I wasn’t aware of debugging, which is very important when it is needed. Thank you @G4BB3R.

I intend to build my app starting with the very simple update. As required by new forces, I can add features. One might be to use a variant such as

type State = String
type Msg = A | B | C
type Message = Regular Msg | Function (State -> State)

Two forces influenced my view. The first was to make the incr / decr example easier for a complete beginner to understand. The second was my long-time experience of Python. A function of type State -> State is analogous to a method on a Python State class.

I think of my app as State and methods first, and as a view second. I think that many who come from HTML + Javascript think of the view as coming first. When is the developer’s focus on the Model, and when on the user experience? This came through clearly in @mattpiz’s post, particular the focus on the event that happened first, and then the intent.

Finally, I’m also a long time TeX user. I was excited in 1990 when Alan Jeffrey discovered that the lambda calculus could be built using only TeX’s macro expansion features! I think this encouraged my approach of startng with

update fn state = fn state

and then adjusting the rest of the app to fit. For Alan Jeffrey’s work see here.

2 Likes

I would advise against going the route of functions in messages.

There used to be a port of material design widgets that used this pattern in order to manage some state automation. Evan (Elm’s creator) spoke against this approach and I would not be surprised at all to discover that in a future version of Elm, this style would no longer be possible.

@pdamoc: Thank you for your advice. I’m reminded that experience is knowledge you get just after you needed it. Is elm-ui-widgets 3.0.0 the port you mentioned? Do you have a link for @evancz speaking against the function instead of message approach?

1 Like

you might find this useful

1 Like

Unfortunately, I don’t seem to find that discussion anymore.

I found a reference from Richard here where he mentions that Evan has said “it’s not a good idea to put functions in Msg”.

Something that is nice about Msg is that since Msg is a defined type, it is basically finite and fully described. Like in…

type Msg = A | B | C

You can just see that it only comes in the varieties A, B, and C. Anyone can wonder “What are the kinds of Msg in this module? I forgot…” and then check the definition and then know the entirety of what could possibly be changing state.

Versus, if you do String or Model -> Model, its impossible to get a mentally complete idea of what is going on in the application. The kinds of things that fit those types are endless, and they could be hiding anywhere in your project.


We have been missing this advantage at work recently, where we have some old but widespread code that is very stringly typed. Particularly api calls, and url changes. So in our meetings where we assess how feasible a project might be, if it touches this stringly typed code we just have throw up our hands and shrug a little bit, because we just cant know what relevant things could be in a string-url or a string-api call. But, if you have a type Route = or a type ApiEndpoint = in your code, all possibilities must be listed there, and you can read them all at a glance.

6 Likes

Thank you. The message from Richard’s reply extremely helpful, and indeed the whole thread. I think this statement sums up the issue very well.

Personally I am beyond excited to use the 0.18 dev tools and have no interest in sacrificing that debugging feature (or others) in pursuit of the local maximum of “less boilerplate.”

This concisely and strongly summarises the debugging case against my suggestion. Here’s the link you kindly gave to Richard’s reply, and thus the whole thread.

This is a long post, but it provides a summary to the thread so far. So please read on.

Thank you @Chadtech , particularly for your description of your work experience. I don’t know what you mean by stringly typed code, but it sounds bad. Certainly, substandard code in a project can obstruct and restrict change. (It’s an example of technical debt.)

That said, incidental technical debt in a throwaway prototype could very well be a good thing. According to Wikipedia

Speed is crucial in implementing a throwaway prototype, since with a limited budget of time and money little can be expended on a prototype that will be discarded.

Indeed, a paper prototype has a human being playing the role of the computer. That’s a debt. Perhaps if there’s an inexpensive transition from the prototype phase to the production phase they’ll be a place for both forms of the update function in Elm.

There, I’ve found a plausible use case for update fn state = fn state which fit’s well with my situation. I’m new to Elm and so want to reduce my immediate learning curve, and I’m creating the first version of an application whose design and success is uncertain. That said, I have to be willing to throw or migrate away technical debt.

Thank you all who’ve contributed. I’ve learnt a lot and it’s been fun.

3 Likes

One of my favorite things about elm is that it makes it extremely easy to “do the right thing” so that the trade-off against speed is much lower than in other languages (in my experience)

One thing that bother me is that you loose the comfort of the types.
I would rather make a big case...of, move all the logic in separate functions and keep a distinct type for each way to update the model.
Sure the update can be quite painful to read, but with some good names and tidy code, it is not that hard to understand how it works when you get used to. And nothing makes me more happy that a well typed code :stuck_out_tongue:

I have the feeling that passing functions to the update for “convenience” is something that I would see in javascript.

One reason to not do it, is that if you build the functions in your view it is easier to accidentally take stale state from the previous Model version. An update always goes form Model to Model, but if you define this function inside a view there will actually be 2 Models so you need to be careful to use the right one:

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick (\msgModel -> { msgModel | count = model.count + 1 }) ] [ text "+1" ]
        , div [] [ text <| String.fromInt model.count ]
        , button [ onClick (\msgModel -> { msgModel | count = model.count - 1 }) ] [ text "-1" ]
        ]

Note that in the above I did model.count + 1, instead of msgModel.count + 1. Normally this does not matter, but in some situation where events are triggered very quickly in succession (say mouse movements), this can lead to stale data from the wrong model.

Doing it in the update function is clearer, since that always starts with the latest version of the Model.

2 Likes

Usually, if my update function gets too big, I split out each part of the case statement into its own function. Another nice way, can be to define an andThen function and use it with good function names so that each case reads naturally. Also its worth mentioning that the update function can be exactly like the specification of a state machine, which is nice - you can design a UI as a state machine, then code it up exactly as you have designed it.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( model.controlCursor, msg ) of  -- controlCursor is the current state.

        ...  -- Lots of cases.

        ( ActiveCursor pos, InsertChar char ) -> 
            -- Each case uses andThen to chain some functions together.
            -- You can read quite naturally what each case does.
            ( model, Cmd.none )
                |> andThen (insertChar char pos) 
                |> andThen (moveCursorColBy 1 pos)
                |> andThen rippleBuffer
                |> andThen activity

        ...


andThen : (model -> ( model, Cmd msg )) -> ( model, Cmd msg ) -> ( model, Cmd msg )
andThen fn ( model, cmd ) =
    let
        ( nextModel, nextCmd ) =
            fn model
    in
    ( nextModel, Cmd.batch [ cmd, nextCmd ] )
2 Likes

That’s a really good point, Chad. Also, when you want to know what an app does, just look at the definition of Msg. It’s all right there.

I’m curious as to how the downsides of having functions inside messages compare to using higher order functions in general.
Personal preference - “I just prefer a big case switch” - is of course fair, and the technical limitations of the debugger are something that I hope will be eventually resolved on the tool’s side; doesn’t everything else (possible semantic errors in lambda functions, hidden information) apply to every function passed as argument? After all a message is just a parameter that the Elm runtime passes to update, and that can be a function in the same way that Maybe.map takes an update operation to hide the explicit switch on the irrelevant case.

I used to do most of my coding in C++. When I moved to python I found, despite loving the formalism of C++, that yes things did seem to be faster to get going.

But I now strongly believe that “rapid prototyping” is an illusion. You might write the code quickly, but you still spend ages debugging/investigating why it isn’t doing what you expected.

Having embraced Elm I now physically cringe every time I have to write something in Python (or other languages). It really irks me just how many times I have to go through the “modify → run → check → find bug → modify → …” loop.

Elm’s incredible type system really does mean that 90% of code works first time. But it’s not just the type system that achieves this - it’s the elm architecture (TEA). Separation of concerns between view and update frees up a huge amount of ‘mental space’ for juggling more important concerns within your program.

When I first started using Elm, I liked the type system but couldn’t really understand that it was that much better than anything else I’d previously done - after all it’s a fairly straightforward and simple concept (even if some types can get quite complex individually). But since then I have found myself writing code that I would never have even attempted before as it would have seemed just too complicated an undertaking. The only way you can really understand the power of TEA and a Elm/Haskell/ML-like type system is to use it for a bit.

One of the dangers of the “rapid prototyping” mindset is this absolute fear of “boilerplate”. But honestly it takes a matter of seconds to write most ‘boilerplate’ in Elm. That’s really not where most of your time is ever going to go in a programming task. We all catch ourselves doing it - “oh this would just be faster if I could just avoid having to write all this first…”, and we nearly always spend more time thinking about it than it would have taken to just type it! It’s truly embarrassing how many times I’ve caught myself doing this!! [I think part of it is that we are (correctly) always looking to eliminate duplicated code, so that we only need to modify things in one place. But boilerplate isn’t really the same as duplicated code, it’s just the formalism of the language we’ve chosen.]

Adding the case statements to an update function, really honestly isn’t going to slow down your experiments in Elm, but it will get you thinking in the TEA approach and the only way you’ll really start to understand the power of that approach is by using it, so I’d really really advise against this idea of starting one way and then redoing things “the proper way” once they get too big. I don’t think it’s helpful in the long term.

Huge apologies if this seems super-opinionated! I just wanted to share how much doing things ‘the right way’ has actually increased my power as a programmer, without my even realising how it was doing so. There really has been a huge amount of thought by Evan and those who contributed to Elm as to why this methodology should be the way Elm works/is supposed to be used.

Finally I’d just add that if update functions get too large for your liking it’s really easy to split them up by areas of concern. for example one way you could do it…

type Msg
    = GameMessage GameMsg
    | SettingsMessage SettingsMsg
    | ChatMessage ChatMsg


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GameMessage gMsg ->
            gameUpdate gMsg model

        SettingsMessage sMsg ->
            settingsUpdate sMsg model

        ChatMessage cMsg ->
            chatUpdate cMsg model


gameUpdate : GameMsg -> Model -> ( Model, Cmd Msg )
gameUpdate gMsg model =
    ...
1 Like

I am begining to discover that one area where putting functions inside messages can be extremely useful is if you have lots of side effects that you want to chain together. You can define an AndThen type of message that will run one side effect and pass its output to another in the next Cmd. That’s a simplification, but its kind of the basis on which elm-procedure 1.1.0 works. This is effectively “task ports” implemented outside of the Elm kernel.

Typically UIs don’t have long chains of side effects - each event might either just update the model, or trigger 1 side effect like making an HTTP call.

I am using Elm to write back-end code on AWS Lambdas, and I find that having many side effects is common in that scenario. For example an incoming HTTP endpoint invocation might read from a database, write to S3, trigger some process, write to a database. If I have to model each step as its own Msg its a pain, and makes writing code like that very time consuming and not so easy to follow the flow. If I can just andThen all the steps together its easier to do and to read the result.

1 Like