For lack of a better name, I’m calling it “The Module Pattern”.
Tags: elm, functional
Published: 2024-03-27 02:24:49

The **Module Pattern** is a pattern I've discovered in Elm for dealing with some of the inconveniences of nested TEA[^1]. As an added bonus, it can make component-type modules a little more straightforward to implement.

> **Note**: I don’t recommend building an application entirely out of nested TEA. In fact, I recommend nesting TEA as infrequently as possible; but sometimes it seems to be inevitable - as much as again when you’re working on a large codebase that isn’t entirely yours. Handle with care, proceed with caution, terms and restrictions may apply, see store for details, something something California okay anyway moving on.

People tend to nest TEA when they "want components"; a common scenario is that a codebase will include a form module - let's call it `Form` - that has some parameterized types and is designed to be "hosted" within another module (usually a "page"). The intent of the author of the `Form` module was that, while a form is inherently stateful, that state should be opaque, and any interactions with the form's state should be mediated through its own `view` and `update` functions, and then mapped back to the host module.

So let's imagine that we have our `Form` module, and let's imagine that we want to add a form to a module called `Signup`, that describes a signup flow. You'd expect to run into something like this:

**`Form`**
```
module Form exposing (Model, Msg, view, update, init)

import Html exposing (Html)

type Model = Model ...

view : Model -> Html msg
view model =
    ...

init : ( Model, Cmd Msg )
init =
    ...

type Msg
    = ...

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

**`Signup`**
```
module Signup exposing (Model, Msg, view, update, init)

import Html exposing (Html)

type Model =
    Model
        { formModel : Form.Model
        ...
        }

view : Model -> Html msg
view ( Model model ) =
    Html.div []
        [ Form.view model.formModel
            |> Html.map GotFormMsg
        ...
        ]

init : ( Model, Cmd Msg )
init =
    Form.init
        |> (\(formModel, formCmd) ->
                ( Model
                    { formModel = formModel
                    , ...
                    }
                , Cmd.batch
                    [ Cmd.map GotFormMsg formCmd
                    , ...
                    ]
                )
           )

type Msg
    = GotFormMsg Form.Msg

update : Msg -> Model -> ( Model, Cmd Msg )
update msg (Model model) =
    case msg of
        GotFormMsg formMsg ->
            Form.update formMsg model.formModel
                |> (\( formModel, formCmd ) ->
                    ( Model { model | formModel = formModel }  
                    , Cmd.map GotFormMsg formCmd
                   )
```

Let's look at all of the plates we just had to boil:

- We had to call `Html.map` once, and `Cmd.map` twice
- We had to destructure the result of `Form.init` and `Form.update` and map them to our host module's `init` and `update`
- When we mapped, we had to map to `GotFormMsg` three times

And this is a trivial example; what if we had to pass outside params to `Form.init`, but not to `Form.update`? What if `Form.view` and `Form.update` both needed access to a `Session` that was defined outside of `Signup`, that needed to be handed in? Bucket-brigading dependency params around to multiple callsites in the same module can quickly become exhausting.

Moreover - what if `Form` was one of those super-cool modules that had a `Config` type that was constructed applicatively, with a dozen exposed functions to manage its various options - and then another score of exposed functions to actually implement bits and pieces of the module?

And finally - for every module within which you choose to implement `Form`, you have to do all of these mapping motions _over and over and over again_. As `Form` grows, if the parameters required to construct it and implement it change, you have to change _every single callsite_ where every MVU touchpoint is accessed, in _every single module_ that uses `Form`.

Oh, and I know that I _just said_ “And finally -“ - but one more thing - maybe the worst thing? - there’s no clear delineation between “the parts of this module that are related to interop with another module”, and “the parts of this module that are actually dealing with business / domain logic”. In fact, it may be the case as this application grows, that your inter-module communication gets woven throughout the rest of your domain logic, sprinkled in haphazardly catch-as-catch-can - and it can become remarkably hard to disentangle, later on down the line.

Now comes the Module Pattern. The idea is that you can create a type that represents your "module" - really, your model / view / update - and expose _that_; by parameterizing a function that "initializes" your module, you can pass in all of those mapping params in one centralized place.

Let's do it for `Form`:

**`Form`**
```
module Form exposing (Model, Msg, Module, init)

import Html exposing (Html)

type alias Module msg model =
    { view : model -> Html msg
    , update : Msg -> model -> ( model, Cmd msg )
    , init : ( Model, Cmd msg )
    }

init :
    { toModel : model -> Model -> model
    , fromModel : model -> Model 
    , toMsg : Msg -> msg
    } -> Module msg model
init { toModel, fromModel, toMsg } =
    { view =
        \model ->
            view (fromModel model)
                |> Html.map toMsg
    , update =
        \msg model ->
            update msg (fromModel model)
                |> ( \( formModel, formCmd ) ->
                    ( toModel model formModel
                    , Cmd.map toMsg formCmd
                    )
    , init =
        Tuple.mapSecond GotFormMsg init_
    }


type Model = Model ...

view : Model -> Html msg
view model =
    ...

type Msg
    = ...

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

So what does that do for our callsites in `Signup`?

**`Signup`**
```
module Signup exposing (Model, Msg, view, update, init)

import Html exposing (Html)

type Model =
    Model
        { formModel : Form.Model
        ...
        }

formModule : Form.Module Msg Model
formModule =
    Form.init
        { toModel =
            \(Model model) formModel ->
                Model { model | formModel = formModel }
        , fromModel =
            \( Model { formModel } ) -> formModel
        , toMsg = GotFormMsg
        }

view : Model -> Html msg
view ( Model model ) =
    Html.div []
        [ formModule.view model
        ...
        ]

init : ( Model, Cmd Msg )
init =
    ( Model { formModel = Tuple.first formModule.init, ... }
    , Cmd.batch [ Tuple.second formModule.init, ... ]
    )


type Msg
    = GotFormMsg Form.Msg

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

It's nice, it's simple, and it gives you some extra leverage:

- You know _for certain_ what interfaces you need to satisfy in order to successfully interop with `Form` - if something is a parameter to `Form.init`, now, you absolutely must have it in-scope to be able to create a `Form`.
- You can think in terms of "the module that you're in" all of the time, unless you're implementing `init` - your `Form` module's `view` and `update` can be written in terms of `Form`, and your `Signup`'s `view` and `update` can be written in terms of `Signup`.
- You can repeat this pattern virtually everywhere, so other developers in your codebase will always know that to implement a module, you always start at `init` and expect a `Module msg model` that they can use to represent that module's data and behaviors, without having to think too hard about how to map data into and out of it.
- You can centralize all of your module’s initialization, the definition of all of its required dependencies and interfaces, and its interop _all in one place_; as your application grows, your `init` will only change as requirements for inter-module communication change, and the rest of your module will only change as business / domain requirements change.

Note, too, that you don't _have_ to return a `view` from your module initialization; you can return data - a function that takes an event and returns HTML, so that you can implement (for instance) form submission via any kind of `onClick`-able element - or maybe just a list of values of a type that your module is creating, so that you can render them however you want.

In my humble opinion, the less frequently you feel as though you need nested state in an Elm application, the better your life is going to be; but if you must do it, this seems to be the cleanest way to go about it.

[^1]: “Nested TEA” is a common pattern in Elm development, where “TEA” - The Elm Architecture, also known as Model-View-Update - is used as the primary abstraction for organizing the overall application. Nested TEA is so-called because most major parts of the application are segmented off into their own modules / module namespaces, with internal model / view / update types and functions that are “nested” inside of other models, other views, and other updates. Whether or not this is actually the best way to build Elm applications may be up for debate; but the fact remains that this is a popular style, and that if you’re a working Elm developer, you will likely get paid actual money to deal with it.