In the last episode we sort of glossed over something I wanted to draw your attention to today, related to recursively calling update.

Project

Here's the relevant code:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        ChatWithUser user ->
            {-
               This is really:

               - Join a channel for this particular 2-way user chat.
            -}
            let
                channel =
                    twoWayChatChannelFor model.username user.name
            in
                update (JoinChannel channel) model

Here, we're saying that when you want to ChatWithUser user it should really map to the same thing as a JoinChannel channel message, where the channel is the twoWayChatChannelFor the two users in the conversation. We just call the update function again, which will act like the runtime gave us that Msg in the first place.

This is pretty nice and I'll typically stop here, but let's talk about alternatives to this; some good, some bad.

Cmd ಠ_ಠ

You could send out a Cmd to the runtime that maps to the appropriate JoinChannel Msg. I've seen code where people took this approach. It's basically awful. I'll explain why, if you didn't immediately recoil.

First off, the code is far more verbose than just doing this. I won't show you the code, because it's terrible and I don't want to encourage this behaviour. Secondly, it's no longer pure - you're sending out a side effect that results in the function call you want, essentially. There's really no reason to do this at all. So strike this alternative, it's awful.

Abstract out the JoinChannel branch

You could turn the interior of the JoinChannel case branch into a function, and call it in both places to manage your update for you. This is not necessarily a bad idea, but in this case I don't think it's the best idea.

Do it in the view

We also could have just gathered the twoWayChatChannelFor this user when rendering the view and emitted a JoinChannel message in the first place, to avoid this indirection. That would look something like this:

--userView : User -> Html Msg
--userView user =
userView : Model -> User -> Html Msg
userView model user =
    let
        { class } =
            Styles.mainNamespace
    in
        div
            [ class [ Styles.RosterUser ]
            --, onClick (ChatWithUser user)
            , onClick (JoinChannel <| twoWayChatChannelFor model.username user)
            ]
            [ text user.name ]

That's ultimately a much simpler solution in this case, and can probably be used most of the time to solve the problem in a simpler way. However, I don't think I'll end up there because I anticipate actually bolting on additional behaviour. I also think it's nice that there's a semantic "place to look" for handling this Msg - it's the kind of Msg that describes the behaviour of the system, and it provides a seam for refactoring later. Also, I don't like that the userView now depends on me threading the model through it - it's relatively unimportant information to that view, semantically, and managing it in the update lets us avoid that.

Summary

So in this case, I think I'll leave it as it is - but it's nice to lay out my reasoning. If you see a flaw in my reasoning or an alternative I haven't covered, please add a comment and let's chat about it! See you soon!

Bonus

In the last episode, you saw a bug where sending a message in the chats did not clear the value from the field. This was due to my trying out defaultValue instead of value. Here's the fix for the offending code:

messageInputView : Model -> Html Msg
messageInputView model =
    let
        { class } =
            Styles.mainNamespace
    in
        form
            [ onSubmit SendMessage
            ]
            [ input
                [ placeholder "Message..."
                , class [ Styles.ChatInput ]
                , onInput SetNewMessage
                --, defaultValue model.newMessage
                , value model.newMessage -- <-- theeeeere we go
                ]
                []
            ]

There's a good discussion suggesting that in cases where defaultValue works for you, you should prefer it. In this case, it does not. From that link, the actual problem I was thinking I might need to avoid appears to have been fixed in elm-lang/html for quite some time.

Josh Adams

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.

  1. Comments for Recursive Update

You must login to comment

You May Also Like