New view API design technique and accompanying blog post

Hello everyone!

A while ago, I stumbled upon a technique that I believe eases the maintenance of views for TEA-style modules, while leaving the module itself conceptually simpler.

I’m writing a post about it for my company’s blog, but I want to take the community’s temperature first, both on the merits of the technique and how well I’m explaining it.

The draft is here

Any feedback you can give is hugely appreciated!!

1 Like

Hi!

Nice read! Some feedback from my experience:

What I missed

I’ve read the post (a couple of times) and personally I’ve had a hard time specially with the “Deferring decisions with higher order functions” section which is actually the meat of the post. I’m familiar with render props so it frustrated me a bit.

I’m not sure what to suggest as I didn’t fully understand. Maybe the code examples could be more concrete about specific use cases like in the previous sections.


Also it would be good to read how this pattern would interact with the performance optimizations like Html.lazy and keyed (if at all different)

What I liked

I think I understood the intro and exhibits, which are nicely laid out, and the interface accretion part is sensible and makes sense.

I liked the benefits and costs summary at the end, that is something we should always do more of.

Also, in my experience I had never seen/thought-of this pattern:

view : Model -> { dashboard : Html Msg, modal : Maybe (Html Msg) }

I’ve personally kept the functions separate -standalone- for consumption from the calling module, but it is a new interesting pattern to see!

Hope this is somewhat useful :smiley:

2 Likes

This is absolutely useful!

I think I see what you’re saying about the middle section. I’m realizing that I only included examples of what the module’s interface looks like, and not what the actual view “implementation” of that interface looks like in Main. Just as a little experiment, here’s a hypothetical Main.view that uses a slightly modified version of the HoF interface to render the dashboard, the modal, and the button for the “Order Fruit” workflow.

Notice that the function I’m passing to Dashboard.view is rendering the entire app. I’m taking advantage of this to render two different types of messages side-by-side (some Html.maping required), and to render a modal at the DOM root.


view : Model -> Html Msg
view model =
    case model.page of
        Dashboard dashboardModel ->
            Dashboard.view dashboardModel
                (\{ upvoteFruit, typeFruitName, addFruit } dashboardView ->
                    case dashboardView of
                        ViewingDashboard ->
                            div [ class "page" ]
                                [ div [ class "dashboard" ]
                                    [ button [ onClick OrderFruit ] [ text "Order " ]
                                    , button [ onClick upvoteFruit ] [ text "Upvote" ]
                                        |> Html.map DashboardMsg
                                    , button [ onClick addFruit ] [ text "Upvote" ]
                                        |> Html.map DashboardMsg
                                    ]
                                ]

                        AddingFuit fruitName ->
                            div [ class "page" ]
                                [ div [ class "dashboard" ] [ ...similar to above... ]
                                , Dialog.view
                                    { body =
                                        input [ type_ "text", onInput typeFruitName, value fruitName ]
                                            []
                                    }
                                ]
                                |> Html.map DashboardMsg
                )

... and so on for OrderFruit

I’m working on a repo with a more complete example to include in the post, but does this at least give you a bit more intuition for what’s going on?

1 Like

Also, I just noticed that the implementation for view : Model -> (Msgs -> View -> Html msg) -> Html msg was wrong, so I fixed it and updated the example to better match what I have above

here’s the fix


type Msg
    = UpvoteFruit
    | AddFruit
    | TypeFruitName String

type alias Msgs =
   { upvoteFruit : Msg
   , addFruit : Msg
   , typeFruitName = String -> Msg
   }


view : Model -> (Msgs -> View -> Html msg) -> Html msg
view (Model state) f =
    f
        { upvoteFruit = UpvoteFruit
        , addFruit = AddFruit
        , typeFruitName = TypeFruitName
        }
        state
1 Like

Both examples help! Looking at the Dashboard.view with the other definitions and code samples in the post makes it more understandable for me.

I see the pattern better with this. While re-reading I’ve noticed that something that caused me confusion was the View type and the dashboardView variable names. I usually would associate view names to things used for producing HTML, so it was a bit confusing. Once I made a mapping from View to State or DashboardModel and from dashboardView to dashboardState or dashboardModel things became clearer, together with the extra examples :+1:

Just curious, why name these as “view”? I’d like to understand the reasoning!

I am finding this hard to follow, I think because I don’t quite understand the problem that’s motivating it. Or I don’t understand the first step that leads you to

view : Model -> { dashboard : Html Msg, modal : Maybe (Html Msg) }

To me, a more common approach is the so-called OutMsg pattern, i.e. to return an extra message in update, which the parent handles. So in this case, based on a certain user action or subscription, craft an OutMsg that contains the data needed to render the modal in the parent. Granted, if you have to wrap and unwrap these OutMsgs up through two modules, it gets to be tedious too, but going through the update rather than the view seems more common (used for example in elm-spa-example), and I wonder if you considered that but felt doesn’t work as well in this case.

I was thinking “View” in the sense that it’s the data structure that you use to ultimately render the view, but I’ve also thought of calling it State. I think you’re right that State conveys the intuition more accurately :slight_smile:

I’ll try it out in the next draft and in the example repo

Correct me if I’m wrong, but I don’t think OutMsg works for the modal. Part of the data needed to render it includes the Dashboard.Msgs that drive the interaction, so assuming you want to hide those from someone who can call your update function, you’re forced to handle it all in your view function, or safely defer the view like I’m proposing.

OK, so I didn’t quite understand, the modals are not generic “Message + OK/Cancel” type things but involve custom interactions that are managed by the Dashboard. In that case why not something like this (not tested):

-- in Main

type Msg
    = DashboardMsg Dashboard.Msg

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        DashboardMsg submsg ->
                let 
                    (newdash, newmodal) = Dashboard.update submsg model.dashboard
                in
                    ( { model | dashboard = newdash, modal = newmodal }
                    , Cmd.none
                    )
        --...

view : Model -> Html Msg
view model =
    -- although I guess this isn't quite right since the modal has to be at the top level and the page within it ?
    div [ class "page" ]
        [ div [ class "dashboard" ]
            [ Dashboard.view model.dashboard  |> Html.map Dashboard.Msg
            ]
        ] ++
        ( case model.modal of
            Nothing ->
                []
            Just m ->
                [ Dashboard.viewModal m |> Html.map Dashboard.Msg ]
        )

-- in Dashboard

type Msg
    = StartFruitOrder  -- or whatever user event triggers the modal

type Modal
    = AddingFruit String

update : Msg -> Model -> (Model, Maybe Modal)
update msg model =
    case msg of 
        StartFruitOrder ->
            ( model, Just (AddingFruit defaultFruit) )

view : Model -> Html Msg
--...

viewModal : Modal -> Html Msg
--...

One thing to note here @xilnocas: it looks like you’re essentially creating a “component”-based architecture which is not the recommended way to structure most Elm apps. I think that adding such a disclaimer somewhere towards the beginning of the post would be a good idea, in case any beginners stumble upon the post and feel the need to emulate what you are doing.

Some more material on this:

1 Like

I respectfully disagree @christian, there is no triplets mention or component references, it is just a pattern with higher order functions for views that serves some specific use cases and has pros and cons (that the article discusses) :+1:

I totally get your article.

You are making use of a higher order function to ‘inject’ some further rendering into a view so that the view itself becomes composable. This is a really nice way of demonstrating how functional languages achieve code re-use; you take a function that you want to re-use and inject into it another function that modifies its behavior. It is somewhat analogous to sub-classing in OO languages, but more concisely handled in functional languages without the OO baggage.

I have done something similar with the content editor that I have been playing around with.

It has 2 modes of operation - server side, where it outputs static HTML from markdown, and client side where the markdown becomes editable when the user interacts with the area of the screen where it is displayed. This allows the same content model to be rendered in ‘read only’ mode or ‘editing’ mode with WYSIWYG editing on the page (eventually… can only edit the markdown now, but it is done in place on the page). I achieved this by injecting the content renderer as a higher order function into my view.

This is a totally legitimate coding pattern in Elm. It does ‘componentize’ things in the sense that you are using the type system to define the shape of higher order functions that describe components. But when you need to achieve complex re-use in your view it can be an elegant way to do it.

1 Like

You are providing the component with a set of Msgs or functions to build Msgs, these are sometimes called taggers in re-usable view code - they tag some action with a Msg. So the components type is:

Msgs -> View -> Html msg

This allows the component to render itself with an appropriate set of Msgs for the context in which it is being rendered. The set of actions “upvoteFruit”, “addFruit” and “typeFruitName” capture the possible output events of the component.

So does this allow you avoid situations where out messages would often be used?

This is definitely a valid way to go about it! It achieves the same goal of letting the caller render the dashboard and the modal in two different places (and your Main.view example is fine, the constraint is that the modal must be a child of the root dom node, not the root itself).

I would argue that it’s a bit more boilerplatey than the “view returning a record approach”, while still suffering from the same interface accretion issues. You’re still having to design your two separate view functions around the fact that the caller has to call them in two different places in the DOM, when in a perfect world, you’d want your module to be agnostic of that fact.

1 Like

I tried to make it clear at the beginning of the article why we’re talking about a TEA-style module (by which I mean a module with its own Model, Msg, init, update, and view), but I can definitely do more, especially to drive home that this isn’t a beginner-friendly article.

In my view, TEA-style is not the best or only way to modularize an elm app, but it’s a pattern you can still find yourself using while following community best practices (elm-spa-example uses it in a couple places, for example).

More speculatively, I think deferring views has the potential to alleviate some of the issues people (including myself) have encountered with “components”, but that remains to be seen.

1 Like

Thanks. My only point is that some people (like myself) may be thinking of this kind of solution using the update instead of the view, and wondering how your approach improves on this. Maybe that’s something you want to raise and expand upon in the article?

I don’t quite follow what’s so bad about that? Modal state belongs in Main since that’s where the modal gets rendered, despite the fact that it’s the Dashboard that triggers a change in modal state. It’s much the same situation as elm-taco addressed it seems to me.

I understand, you are trying a different approach, it’s just I feel like the article needs an argument for why this improves on things like elm-taco and OutMsg, or in what situations it might be better. Changing what a view function returns seems like a radical step – it isn’t really, but I don’t think it’s really been explored before, to my knowledge.

PS. Quite apart from any of this, I will be glad when modal dialog box style interfaces are seen as passe and kept to an absolute minimum. Not just because of this technical issue but because they are hard to make accessible, not to mention being a holdover from before undo was invented. Just sayin’ … :wink:

Thank you for the thoughtful replies! Let me try and address your real concern, so far I’ve been totally glossing over it, I think because I’ve been struggling to put to words some of the ideas around my reasoning.

how does view : Model -> { dashboard : Html Msg, modal : Maybe (Html Msg) } improve on an OutMsg/Taco style solution?

  • On a conceptual level, it eliminates the need for the Modal to be its own stateful entity in the app. The presence and appearance of the modal is just a pure function of the Dashboard’s state. To me, this is more in keeping with the view = f(application state) philosophy. I don’t have to think about modeling a modal, or where in the document it happens to be, I just model the interactions my application supports, and then write down a function that describes how steps in that interaction map on to the visual concept of a modal.
  • On a practical level, it’s simply less code to worry about: No more threading OutMsgs through Dashboard.update. And if you want to swap the modal out for something else more user friendly, you just have to change the view function, rather than changing the view and a bunch of wiring.

I fully concede that sometimes, the thing you’re sending OutMsgs to really is another stateful entity in the app. The User type in elm-spa-example is a perfect example. In those situations, OutMsg is fine, as is using an extensible record to update the shared state in the submodule’s update function.

I will definitely include some sort of comparison to OutMsg in the next version of the post, I now agree that it’s what lots of people reading will wonder about.

I’m mulling over the approach this ends up at. At the very least, I like it as a way to build a clean boundary between model/update code and view code since the exposure of the State definition does not expose the Model definition even if the model is just a thin wrapper around the State. Restricting where messages can be accessed is interesting but I’m less certain that’s valuable since one might want them exposed for testing.

In the meantime, I can concur that having the view function return a record with the main view and an optional modal makes perfect sense. I’ve done something similar when needing to populate a header. One could simply expose view and viewModal but returning both views from one function emphasizes to the caller that the view for the dashboard in your example comes in two parts and both parts need to be included in the view hierarchy in some way. That seems exactly like what a good module API should do in pushing clients to account for what the module itself does and what it needs its caller to handle. (Cmd and OutMsg are basically requests for action beyond the scope of the module. Tacos are specifications of additional context needed to perform the task but not supplied by the module.)

Mark

1 Like

Hey @xilnocas,

I appreciate how much time you put in your post, as well as all of the thoughtful comments.

Just my two cents having spent the last 6 months building out a fairly large Elm app (15,000+ LOC).

From a high level, I think you are going to regret it if you start to build a large Elm app and do what you are proposing.

Even the very first concept leads down a very dark path. The idea that you would immediately split out an elm app along the lines you propose will likely lead to much pain and suffering.

I’m not a big poster online, but think you may find this useful:

(My re-write of elm-taco, another proposal for scaling Elm apps. I have never posted a link to it before.)

I just say this from recently bringing on developers who are joyful at how easy it is to add new code and understand what is going on with our fairly complex codebase. We follow elm-taco-donut exactly, and it worked (and works!) like a charm.

As soon as you start getting stuff like:

view : Model -> (Msgs -> View -> Html msg) -> Html msg

you are going to be unhappy.

All our views are

view : Model -> Html Msg

(including Modals, Overlays, Alerts, Pages, etc.)

Super simple. Update the model, and the views change! Voila!

I would just feel bad for someone to stumble across your well-meaning post and think that this was a “good” way to build Elm apps.

You will be fighting TEA the whole way.

Personally, I think one of the hardest problems people have with Elm is that they get scared of big files. Because in other languages, big files lead to big problems. So there is an urge to chop up files into “components/bits/whatever”.

Unfortunately, this often fights the very nature of Elm’s central message bus, and of Elm’s idea of a central Model.

The fact that you create a new thing called State should be a hint that maybe this is a dark road indeed! Model is the state. That’s the whole point. The model can be as big are you like, with as many Lists, Records, Dicts, etc as you need. There is no benefit to breaking it up, and many reasons not to.

Also, there should only be one thing parsing update messages. That’s the point of TEA. That’s what keeps it sane.

Anyway, sorry for the blunt feedback. I rarely post online, but this an area of much personal experience.

My comments are purely practical, and I apologize in advance if they are unwelcome.

Rock on,

2 Likes

Why even call it elm-taco-donut, when the first thing you essentially say is that you believe elm-taco is wrong?

I will also note that asserting that big flat models are safe because of immutability is also wrong. If there are inter-field invariants that can’t be expressed through single data types, then there are plenty of opportunities to mess up the model. You just can’t destroy the previous value of the model but since the update loop will throw that out as soon as your update function returns, that really isn’t much if any help. Comparing it to a spreadsheet ignores the fact that spreadsheets contain a lot of logic to automatically and efficiently propagate updates —logic that in Elm has to be written by hand.(*)

Mark

(*) I wrote a native module recently to do essentially what Html.Lazy does for view construction but to do it for arbitrary functions. So, for example, one can take a function f : a -> b and produce a new function with the same type signature which performs exactly the same computation but which does a referential equality check on its argument as compared to the previous invocation and if so it returns the same result. I also created versions for longer argument lists. This is basically what you need to really leverage the spreadsheet angle. Now, your computed spreadsheet cells can be represented as calls to these caching functions with the caching functions also stored in the model “cells” in case you need to use the same function more than once. The net effect of this addition was that I was able to remove a lot of manual change propagation logic and make the program more “functional” while preserving performance. I would publish this code, but native code isn’t viable for general sharing within the Elm community. :frowning:

1 Like