Should a module expose its Msg constructors?

I have a ParentModule.update function, which in turn calls ChildModule.update function.

Nothing weird, very basic, very normal The Elm Architecture stuff.

Should ChildModule expose the constructors for its Msg type?
Should the ParentModule.update function be able to inspect the child’s messages?
Should the Msg constructors be considered part of the interface, or should the type be opaque?

Does anyone have actual experience with a pattern where a parent update function eavesdrops on the child update messages?

Is this a good way for the child update to pass information back to the parent update?
If it is not, what can go wrong? What problems can it create?

The alternative is for the child update to return, together with its Model, a custom type describing what the child wants from the parent, which I instinctively find a more elegant solution, but has the drawback of making the child make more assumptions about the parent.

I would particularly appreciate answers based in actual experience with different patterns.

Thank you.

1 Like

Might need to see some actual code, but I often think that setting up a parent-child system like this, in order to encapsulate functionality in 2 separate modules is not a good idea. Especially since you are talking about 1 of those modules knowing about what is going on in the other. The parent is inspecting and reacting to the childs messages; the child is using messages to communicate with the parent. You have an idea that you would like to encapsulate the design into separate modules, but doing so will present you with problems as the application evolves.

I would suggest combining the entire Model and Msg of the parent-child structure into a single flattened out application. As the application grows, you may want to split out parts of the update logic or view logic into other modules. Doing this will likely require that you pull out the Msg into its own module, for the simple reason that Elm does not allow circular dependencies.

After doing that, you can split out view logic into child modules fairly easily. You might group view logic by areas of the UI that seem like natural groupings to you. Update logic can also be split out by natural groupings of areas of functionality but does not necessarily ahve to align with how the view is split up.

If Msg gets too big, you can also split out subsets of Msg into the child modules where you are sure that a group of them is only ever used in one module, so it makes sense to put them there. Since they are only used within 1 module, the constructors should not be exposed when doing this.

Each area of view of view or udate will likely only need a subset of the fields in the overall Model. You can split out the subset of fields using extensible records:

module Parent exposing (main)

import User

type alias Model = 
    { username : String
    , apiKey : String
    ...
    -- Applications will tend to gather lots of fields.
    }

update model msg = 
    case msg of
        EnteredLoginDetails user pass ->
            -- Can pass down Model here, as the extensible User type alias matches some of its fields.
            User.login user pass model

        ...

=======================

module Msg exposing (Msg(..))

-- Split out as Parent and User and other modules will use it.
type Msg
    = -- All messages in the application, this may grow quite large.

=======================

module User exposing (login, handleLoginResponse, showUserId)
-- Not that the User type alias is not exposed. It just needs to match a subset of fields
-- from the Parent.Model and the compiler will ensure that it does.

-- I always name the extensible record subset the same as the module 
-- as modules should be structured around types.
type alias User a = 
    { a | username : String
    , apiKey : String
    }

-- This is update logic as it follows a ... -> model -> (model, Cmd msg) pattern.
login : String -> String -> User a -> (User a, Cmd Msg)
login username password model =
    -- Create an HTTP request to log in
    ...

-- This is update logic as it follows a ... -> model -> (model, Cmd msg) pattern.
handleLoginResponse : Result Http.Error String -> User a -> (User a, Cmd Msg)
handleLoginResponse response model =
    -- Keep the API key
    ...

-- This is view logic as it follows a ... -> model -> Html msg pattern.
showUserId : User a -> Html Msg
showUserId model =
    -- Show the username and a logout button
    ...

Avoid so-called ‘out messages’, where child modules message parent ones. Over encapsulation can lead to doing crazy stuff like modules messaging each other with out messages to get/set fields on them. Its not OO programming.

Generally use encapsulation techniques more in Elm packages and much less in applications. Its ok for applications to follow a more flat and flexible design philosophy in Elm. Applications will tend to grow lots of fields that will end up being used in places you did not originally expect them to. Flat and open keeps things flexible in this regard making your application easy to scale.

Customary link to the best guide on this way of working: Elm Europe 2017 - Richard Feldman - Scaling Elm Apps - YouTube

5 Likes

I think I have been unclear.

I don’t have a problem with one particular module, I’m trying to figure out a general rule to manage very large projects (100K+ LOC) that will likely share a lot of code but use it differently.

To this extent, reusability is very important and putting everything in a single module is not an option.

“Out messages” are very much not OO programming, because they can be produced only by an update call, so there is zero opportunity for the “back and forth” you mention.

Are you refactoring an existing 100K LOC project? Or starting work on a new project that may grow that large?

If you are starting a new project, I would say put everything in a single module to begin with, but just as a way of starting out and obviously not for the entire project! Once you get to maybe 2000 lines of code, it will begin to become more clear what the structure of the project is, and then you can start making decisions about how to split things out into modules based on the real structure of the code that is naturally emerging. I think imposing a structure up front is risky, because it will likely turn out to be the wrong structure.

Recently, I got to 10K lines before splitting out any modules, and it was a very helpful exercise to do. I admit, it did get a bit painful to work with such a large file towards the end.

I particularly recommend the technique of keeping the model as a flat record where possible, and pulling out modules around slices of the model, by using extensible records. If I were to recommend a general rule for scaling Elm to 100K+, that would be it.

“reusability is very important”

Are you sure about that?

I used to think that, because I used to be an OO programmer, and OO design is often about designing code for maximum re-use.

I find that most parts of an application in Elm are not re-used. Some parts are, and I generally pull those out as I discover them, and often turn them into packages to be published. But yes, I do also make re-usable modules within an application that get used in many places.

I now never make the assumption that something will be re-used up front. In the first instance I will write something like it is going to be used only once, and refactor when I really do discover that something is a good candidate for re-use. It is also worth noting that re-usable code in Elm does not automatically imply making a full child TEA module with its own Model and Msg. For example:

fancyInput : (String -> msg) -> Html msg
fancyInput tagger = 
    -- Draw a fancy looking input.
    -- Capture the input with `onInput tagger`
    ...

This hooks into the Msg type of the caller. Higher order functions are one technique for making code re-usable in many different situations.

Yes, out messages are not specifically OO programming, but I do think they arise from OO thinking mis-applied to Elm.

If you split your model into two modules, each with its own Model and update, you may find that one of these modules needs to know about the state of the other, or send commands to tell the other to change its state. That then drives you to start using out messages. So the out messages were caused by the instinct to encapsulate state in an OO like way.

Elm does not force you to put data and code together into units, the way objects do. Generally its a good idea to structure modules around types. But its also perfectly ok for application code to not always try and encapsulate code and data together and use modules like they are classes. You can have code in many modules work on the same data, and extensible records let you do that whilst tightening what parts of the state are visible to a given set of functions grouped into a module.

The flatter data model means that you don’t need out messages to communicate between modules, they can all see the same state.

I am currently working on a 100K LOC project which I had no hand in designing, and have added another 15-20K lines to it. The original code is componentized into modules, each with its own Model, Msg, update, and maybe view. So one module for invoking an API and caching results, one for some view widget etc. The splitting up of the Model accross many modules has led to extensive use of out messages. So a UI module is sending an out message which is routed to an API module to fetch some remote data (or get it from its cache), and then another out message with the result is being routed back to the original module.

Be smart, and don’t go down this route - but I think you sort of have to to learn from the mistake :man_shrugging: Also, watch Richard’s talk, if you didn’t already, he describes all the techniques for making Elm code re-usable, that you should explore before you use the nested TEA pattern.

3 Likes

Hi @xarvh!

For me it makes a lot of sense to replicate TEA on child modules, each of them with it’s own Msg, Model, update, and view. I tend to keep the state of this components transient, when the user visits a certain route this component gets initialized possibly using URL params. In this way the main module doesn’t have to know about of the specifics of the child components, which can grow independently as a tiny elm app, and the data the model holds represents more closely the correct state of the app, in my opinion it doesn’t make sense for the topmost model to have keys for each of the components specially when this component is not active, in that sense I try to reduce the cardinality.

As for exposing Msg constructors, I would say that’s internal to the module. Encapsulation and code organization by concerns is not exclusive domain of OO and it is encouraged in Elm by the use of constructor functions, phantom types, extensible records, parameterized types and API design.
What I found is that a lot more thought has to be put in Elm when designing APIs in relation to more dynamic languages.
I’ve participated in code bases that either organize code in a Rails inspired way where Model and Msg are kept in shareable modules, or that keep everything in a single huge file. I wasn’t happy either way. I personally cannot deal with the cognitive burden of files with thousands of lines or functions and type definitions bigger than a few lines.

You may have come across discussions about the Translator and the OutMsg patterns.

Browser.Navigation already provides a nice way to implicitly pass messages from a child to a parent.
For explicit message passing in one of my side projects I’ve made a variation on the Translator pattern of which I am very satisfied.
Each child module implements a outerMsg function that maps it’s internal Msg to OuterMsg, similar to Cmd.map and Html.map.
In my case there’s only two things the parent has to know, flash notification bar message set and dismiss, and Http request failure handling, each child only has to map the relevant messages.

-- OuterMsg.elm
module OuterMsg exposing (OuterMsg(..))
...

type OuterMsg
    = RequestFailed Error
    | NotificationChanged Notification.Msg
    | Pass


-- Main.elm
import Child exposing
    (Child
    ...
    )


type Msg
    = ChildChanged Child Child.Msg
    ...


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ...

        ChildChanged child innerMsg ->
            let
                (child, childCmd) = Child.update child innerMsg
            in
                -- route wraps state for a child at a time, it will be built by navigation messages.
                ( { model | route = ChildRoute child }
                , Cmd.map ChildChanged childCmd
                )
                    |> handleOuterMsg (Child.outerMsg innerMsg)


handleOuterMsg : OuterMsg -> ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
handleOuterMsg msg ( model, cmd ) =
    case msg of
        OuterMsg.RequestFailed err ->
            -- Do something
            ...

        OuterMsg.NotificationChanged notificationMsg ->
            ...

        OuterMsg.Pass ->
            ( model, cmd )



-- Child.elm
module Child exposing (Msg, outerMsg, update, view)


update : Msg -> ( Child, Cmd Msg )
update msg child =
    case msg of
        ...

        NotificationChanged _ ->
            ( child, Cmd.none )

        RequestFailed _ ->
            ( child, Cmd.none )


outerMsg : Msg -> OuterMsg
outerMsg msg =
    case msg of
        RequestFailed err ->
            OuterMsg.RequestFailed err

        NotificationChanged innerMsg ->
            OuterMsg.NotificationChanged innerMsg

        _ ->
            OuterMsg.Pass

I’ve simplified above but here’s the in-progress app I am building.

2 Likes

Are you refactoring an existing 100K LOC project?
Or starting work on a new project that may grow that large?

I did the first, and am starting the second.

I have very little formal training or experience with OO programming.

Are you sure about that?

Yes. I am.

Be smart, and don’t go down this route - but I think you sort of have to to learn from the mistake

This is not very charitable of you; our solutions have been working well, in production, for a few years now.

Thank you.

I am familiar with a couple of different incarnations of OutMsg, and my usual approach is something like that.

However I am working with a codebase that uses the child Msg as a way to communicate with the parent, and while I don’t see any horrible downside, I feel like it doesn’t define very well the boundary (or the interface) of the module.

Wouldn’t that require the child to know what OuterMsgs are?

Exactly, OuterMsg defines an the interface that the children are to use to communicate with the parent, and outerMsg function maps the child message to something the parent knows how to deal with.

In my case these children take over the view one at a time as pages, that’s why they’re mounted in a Route type, the possible messages they send the parent is limited and the parent deals with all of them the same way, that’s why I can just pass the (Model, Cmd Msg) tuple to the same handleOuterMsg function which takes the mapped OuterMsg.

The main module is essentially a router with a couple of children common to all pages, such as a side menu, an authentication form and a flash message.

Most of the child-parent messaging is performed by url changes but for messaging when it doesn’t make sense to change the url, such as dismissing a flash message or popping up the authentication form when a request fails, I use the OuterMsg interface.
Not all children require sending a message to the parent, and is just sufficient with updating their state with their own update function, and mapping their cmd.

So far my OuterMsg type has two meaningful variants and a no operation one, it might grow in the future.

The reasons I came to like this approach are:

  • Common messaging interface for children, I can check OuterMsg type and see all of the possible child-parent messages
  • Not littering my parent app Msg with child-parent specific messages
  • In the outermost app module there is an update function dealing with local Msg, and a secondary handleOuterMsg update function dealing only with children-parent messages.
  • Children don’t expose their specifics

Of course there is no correct answer, and I might come to re-examine my choices, but this has worked out quite nicely for me so far.

For reusable components where a common messaging interface doesn’t make sense, the Translator pattern might be a good choice.

Apologies if my response did not come accross well, I certainly did not intend to imply that you are not smart.

1 Like

I would like to have another go at making a case against the concept of communicating modules.

In Elm, you have an update which is a pure function. It is completely deterministic and as the application state is always explicit in the Model, it is generally much easier to reason about Elm programs than Javascript ones. The runtime is also single threaded, so there should be no race conditions or other difficult to reason about concurrency issues in the code.

Race conditions still exist in Elm programs, because the UI is about interfacing to the real world, and the real world is concurrent (The trickiest Elm bug I’ve ever seen | by Coury Ditch | Real Kinetic Blog). Events arriving into the update function can be stale. This can be dealt with by using a state machine, to ensure events are only processed when in the correct state.

If you introduce a parent-child TEA structure with out messages, then on top of this very simple idea you are trying to build an actor model (Actor model - Wikipedia):

" The actor model in computer science is a mathematical model of concurrent computation that treats actor as the universal primitive of concurrent computation. In response to a message it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received. Actors may modify their own private state, but can only affect each other indirectly through messaging (removing the need for lock-based synchronization)."

Its a great model for concurrency as per Erlang (and Elixir?). But when imposed onto an Elm application, it is a conceptual model that does not actually align to the reality of TEA - since Elm is single threaded.

The model likely has appeal to Elm programmers because of locality. It allows you to write modules each of which owns a smaller piece of the overall state and keeps it locally, where it is easier to reason about - something you really need in concurrent programming, whether its Erlang actor model, or Java threads. I would argue that since Elm is 100% immutable there is no need for locality - you do not have to worry that some other part of the program is going to change something.

The communicating modules paradigm in Elm gets used to either yield updates on private state in some module, or to request that other modules perform some update or side effect.

The best way of dealing with private module state in an Elm application, is to simply not have private module state. Pull it up in your overall model, so that it is visible everywhere it is needed.

So you could have a UserAuth module in Elm, which captures the current user id and API tokens, encapsulates all backend communication inside the module and its update function, uses out messages to communicate changes to the users auth status. But other parts of the application will need to know about the auth status, so pull it up into the top-level model. Then build your UserAuth module around a slice of that using extensible records:

module UserAuth exposing (UserAuth, Msg, update)

-- Modules are best built around a type. In this case the type is a slice
-- of the overall application Model. I always name it the same as the module
-- instead of calling it Model.
type alias UserAuth a =
    { a | username : String
    , token : String
    , status : AuthStatus
    }

If you expose the Msg constructors from this module so that other modules can request state from it, or ask it to perform side effects, it just complicates the update function. With the Msg constructors exposed, it might look like:

module UserAuth exposing (UserAuth, Msg(..), update)

type Msg
    = LogIn String String
    | LoginResponse (Result Http.Error UserProfile)

update : Msg -> UserAuth a -> (UserAuth a, Cmd Msg)
update msg =
    case msg of 
        Login username pass -> ...
      
        LoginResponse result -> ...  

There is no need to handle the login through the udpate function. Its just a function, it can be called anything you like, and you can have as many of them as you like. If you thinkg about it, you don’t write other functions with a generic name and request what they compute by passing them custom type constructors, so why do it with update? update is best kept for the sole purpose of handling events arriving from the outside world via Elms runtime.

Without the Msg constructor exposed, it might look like:

module UserAuth exposing (UserAuth, Msg, update, login)

type Msg
    = LoginResponse (Result Http.Error UserProfile)

-- For handling runtime side effects only.
update : Msg -> UserAuth a -> (UserAuth a, Cmd Msg)
update msg =
    case msg of       
        LoginResponse result -> ...  

-- Another update-like function that helps other modules to request a side effect.
login : String -> String -> UserAuth a -> (UserAuth a, Cmd Msg)
login username pass =
    ...

I have seen code that takes the actor model and encapsulation even further, so that you have modules making requests for state from other modules like this:

module UserAuth exposing (Model, Msg(..), MsgOut(..), update)

type Msg
    = ...
    | GetStatus

type MsgOut 
    = ...
    | ReportStatus AuthStatus

type Model
    = Model 
        { username : String
        , token : String
        , status : AuthStatus
        }

update : Msg -> Model -> (Model, Cmd Msg, MsgOut)
update msg =
    case msg of 
        GetStatus -> ...

and even built a routing framework on top of this, so that communicating modules can have ids and message each other over a kind of bus.

The problem with this is that it can introduce non-determinism into an Elm program where there should be none. What happens when a ReportStatus message races against a LoginResponse message? It can be dealt with using a state machine, same as other UI race conditions, but it is going to come at a cost of adding more and completely unnecessary states to the state machine.

The resulting application is less deterministic than Elm ideally allows it to be, and is harder to understand and debug. Applications tend to get pushed down that route by imposing a communicating modules concept on top of Elm. To see more clearly and simply, we need to free outselves of that illusion. That is why I am so down on out messages and encapsulation in applications.

3 Likes