Understanding the purpose of "init" and improving app design

I recently asked a question in the beginner channel on Elm Slack and the really great answers led me to further speculation about the function and behaviour of the init function in Main.elm.

If our Model has many fields but we only want to init a subset of those how can we tell the compiler to only look for those?

The question can be restated as:

Why does init demand that we instantiate the entire Model when we might only want to instantiate a tiny piece of our program - which might be a UI shell waiting for user interaction?

The answers have led me to ask if the following is correct:

  • Does init demand the entire Model be presented to it in some way because its function is to instantiate the entire program state into the Elm Runtime and construct the Virtual Dom?
  • If we do not wish to duplicate the entire Model into init. If we do not wish to instantiate all of our state at startup. And we do not wish to hack a work-around to this requirement by init to present everything that exists in the Model (now and in the future as we add to the program) is there one particular pattern or solution that has risen to the top of best practices in the Elm community to solve this?
  • Does this best practice include the use of Modules to convert Main.elm into a high-level FSM using Elm’s type system? Also making impossible states impossible and providing guarantees for application startup? Where the Model is turned into a Custom Type that allows us to present only that part of the program we wish to instantiate at startup?

There are four Elm talks that come to mind in all this:

Charlie Koster created an article on Medium that adds to this approach, although he writes that it was just an idea and a work in progress at the time.

I understood Life of a File as a recommendation to only move to Modules when data naturally evolved into groups of types that could be moved out of Main.elm together. However, it seems that, especially if we use something like domain-driven type design, and given the considerations that Evan draws our attention to, it might also be good to move to using Modules and more sophisticated patterns sooner rather than later (or as a matter of course in contemporary Elm practices).

This is a very beginner minded exploration so I’ll edit this OP as required to improve clarity of what I’m asking as responses are posted.

Does init demand the entire Model be presented to it in some way because its function is to instantiate the entire program state into the Elm Runtime and construct the Virtual Dom?

This isn’t special to init, Elm doesn’t have the concept of null or undefined. If you want to create a value it needs to be the whole value.

if we do not wish to duplicate the entire Model into init . If we do not wish to instantiate all of our state at startup. And we do not wish to hack a work-around to this requirement by init to present everything that exists in the Model (now and in the future as we add to the program) is there one particular pattern or solution that has risen to the top of best practices in the Elm community to solve this?

If you have fields in a record value that you don’t have a value for yet then you can define them as a Maybe a which is a type that has two variants Just a and Nothing, Just a can hold a value and Nothing can be used if you don’t have a value.
This can be cumbersome when you have a lot of fields that are Maybe a. So it’s common to define a custom type for the various ‘states’ that the value can be in. ‘Slaying a UI-Antipattern’ is a great example of doing just that.

Of course all of this can be nested. You can have a record with fields that are Custom Types for the various states of the value in that field, and the variants of that custom type can hold records with fields that are also custom types. etc.

Does this best practice include the use of Modules to convert Main.elm into a high-level FSM using Elm’s type system? Also making impossible states impossible and providing guarantees for application startup? Where the Model is turned into a Custom Type that allows us to present only that part of the program we wish to instantiate at startup?

You don’t need modules to define Custom Types. You can define all the types and functions you want in a single module. Modules are just a way to group related types and functions. You can write your whole program in a single module and the functions and types won’t be different at all.

Modules provide some namespacing, the ability to hide some internal implementation details by not exposing some types or functions, and improve compiler performance by allowing the compiler to compile a module at a time.

2 Likes

Thanks @jessta. I’m inclined to use Maybe as a last resort.

My sense of Modules included their use as a way of isolating details and implementation of state to help simplify nesting and keep Type hierarchies to a minimum, but it may be from a misunderstanding. But, if there are really great patterns very suited to Elm for defining elegant nested data and Type hierarchies then that is something I absolutely want to explore. It seems difficult to avoid hierarchical and relational data structures the more one defined strictly Typed entities. However, recommendations in the Docs and community have confused my understanding of what is and is not a best practice.

In another discourse thread I wanted to understand the use of Extensible Records in support of more sophisticated, relational data structures (domain entities and domain objects). Again, because of the recommended practice in Elm to keep nesting and hierarchies as simple as possible.

The way I tend to do this is with an init state machine. For example, something like this:

type alias Model =
    { state : State
    , ... -- Maybe some more fields that always exist in every state.
    }

type State
   = InitStart { config : Config }
   | InitGotViewport { config : Config, viewport : Viewport }
   | Running { config : Config, viewport : Viewport, user : User }

Then I can have the initfunction just create InitStart with Config (passed into the flags). Then some more side effects can be run later to pull in more initialization state, until everything needed to transition into Running has been acquired.

If the intitialization sequence is particularly large, it could go in its own module. My current project has an overall type splitting into Initializing and Running (as below) each with their own models, as the init sequence is pretty big (7 states and about 800 lines) - big enough to justify getting its own module. Generally, no particular need to split into modules to use an initialization state machine, other than the factors that normally drive you to module splitting.

type State
    = Initializing InitModel
    | Running RunningModel

My init sequence typically adds fields to records at each state. For the example at the top:

{ config : Config } -->
{ config : Config, viewport : Viewport } -->
{ config : Config, viewport : Viewport, user : User }

-- Also worth noting that these records all type check 
-- against this extensible record type:

type alias Configured a =
    { a | config : Config }

-- So writing functions over Configured can be useful, 
-- and can be applied to any state. 
-- Similarly for other subsets of the state fields.

Elm used to let you add fields to records, but this was removed in 0.19.0 (or was it 0.18.0?). If that was still available I could have done:

update msg model =
    case (msg, model.state) of
        ...
        (Viewport viewport, InitStart start) -> 
            { model | state = InitGotViewPort { start | viewport = viewport } }
        ...

Which works quite neatly for this pattern. Unfortunately we lost that, so I end up copying all the fields over the state transitions, then adding the new ones:

update msg model =
    case (msg, model.state) of
        ...
        (Viewport viewport, InitStart start) -> 
            { model | state = InitGotViewPort { config = start.config, viewport = viewport } }
        ...

Not too bad with only 2 fields, but as the number of fields builds up it gets a bit tedious. In my current application, entering the Running state requires 30 fields. Could I handle this in a different way that lets me add more fields easily, but also does not make accessing them a pain? Not sure but interested in hearing some ideas.

3 Likes

With some wrapping like this package provides you can have an init function which returns a Task, which means you can handle loading and potential errors in a pretty clean way (including json decoding errors from flags). I found it pretty nice, although the downside is you can’t do requests in parallell.

1 Like

The core operational cycle in elm is the update loop:

  1. Runtime sends Msg and Model to update
  2. update returns an updated Model and Cmd msg to the Runtime.
  3. Goto 1

In order for this cycle to start, something has to create a Model. We call that something init.

  1. Runtime sends Flags to init
  2. init returns a new Model and Cmd msg to the Runtime
  3. Runtime sends Msg and Model to update
  4. update returns an updated Model and Cmd msg to the Runtime.
  5. Goto 3

If we were to ask init to produce less information than a full Model, then somewhere else we would have to have another function that turned that first thing into a Model later in order to get the update loop started. But there’s not really an end to that once you start digging; every state might need a different data shape. So instead, Elm insists that if there are different states (LoadingInitialData, ReadyToDisplay) then those have to be representable within Model.

4 Likes

There was this post a while back about putting an entire application in one big file:

Inspired by this I worked on an application where I put >5K lines in a single file. The intention was not to keep it that way forever, but to experiment with the idea of building up a lot of code before deciding how to split it up into modules, so that the splitting up was informed by the application design I arrived at naturally, rather than being imposed onto it according to some pattern I decided on before hand.

Managing the 5K file got tricky and eventually began to slow me down. I could really have done with better editor support (maybe that does exist I just didn’t have time to investigate what is available, was using vscode).

Overally, I would say that this has been a really useful exercise for me. I now have this application split up into 20+ modules. I have not needed to create any nested TEA components. I have not used any ‘out messages’ and now strongly consider this an anti-pattern in Elm. I have used extensible records to take slices of a big model, and build modules around those types, which is a very flexible way to manage things. It is also very easy to change the slicing at a later time, since the model is flat and you don’t have to unwrap/rewrap everything to arrive at a new model design.

There are domain models within this, which usually get their own module, or a cluster of them together in a single module. Domains types are often opaque with ‘smart constructors’.

So I would say the opposite, move to modules once the justification for doing so becomes clear and is guided by the actual application design. Use the simplest patterns you can - Elm generally makes more sophisticated patterns more expensive to use. Its easy to be too clever and end up with code that is just harder to write and manage than it should be. KISS - keep it simple stupid!

2 Likes

Wonderful :heart: This is what I’d like to understand.

What does your init function look like when paired with the your state machine?

@jhbrown While regular functions can choose to work on only the part of the Model they want to modify, and output a new Model in their type signature (as does init) it seems that init is unique in requiring that everything that exists in the Model is declared in its function. A bit like a case that insists on dealing with every variant.

Given that init is unique in this way it seems that we are forced to do one of two things:

  • Use Maybe to allow a Type to remain valueless (or stateless?)
  • Pass init a separate subset of the Model that just deals with initialisation.

If we can create Types and functions that don’t require Maybe it would seem that init may force unnecessary use of Maybe.

Like Bool, it has often been said that the use of Maybe should be kept to specific use cases and minimised if possible.

So it would seem that the only viable solution is to eventually implement a separate subset of the Model just for initialisation.

Interestingly, this brings its own problems, such as requiring the initialisation state be somehow integrated into the running Model (as indicated by Rupert).

elm-spa-example has a very different approach and structure to what the Docs recommend. Richard’s init is embedded in a let in his Api.elm module. Wow. I think I’ll just stick to using Maybe until I get more experience. Thanks, everyone, for the really great responses :pray:

update is transformative over Model. init on the other hand is responsible for creating the entirety of Model since it is, well, initializing things. There’s no sensible interpretation within the Elm type system for not creating an entire Model. I think maybe some concrete examples would help make sense of it in the context of exploring what another language would look like – I’m really not sure what it is you want to do in init, but it’s just not feasible within Elm.

Sticking with the example I gave above, the init function would create the first state of the init machine, and fire off a Cmd to fetch whatever is needed to get to the next state:

init flags = 
    let 
        config = Decode.decodeValue configDecoder flags 
            |> Result.withDefault defaultConfig
    in
    ({ state = InitStart { config = config } } -- Starting state of the model.
    , Task.perform GotViewport Browser.Dom.getViewport -- GotViewport will advance the state machine.
    )
2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.