Code structure/architecture

When I first started with Elm, I split code related to a page into View.elm, Main.elm, Model.elm, Commands.elm and Decoder.elm, if I remember correctly. I later discovered the elm-spa example (https://github.com/rtfeldman/elm-spa-example), and refactored my code to this. Most of the time, I think it works better, because I don’t have to import that much, and I have less files. But for one of my more complex pages, I have a file over 1300 lines, after I import some of the views. For me, this becomes to messy.

I am considering breaking this module up into the previous structure (separate files), but this will lead to inconsistency in the way I structure the code. My other option is to try to break it up into more independent modules, but I am having a hard time figuring out how without having to duplicate a lot of code.

My question is then if somebody has any thought about how I can solve it? I am also interested in learn peoples arguments for the different types of structure of the code.

5 Likes

Hello!

There are some videos on the topic - I recommend them both, they’re great for getting an intuition into a good module organization:

and the consensus seems to be on “everything in one file,” until that starts being too cumbersome, and not splitting concerns (decoder, view, …) into modules but splitting data types and their associated functions into modules.


Anecdote:

At our company we have tried splitting one module with huge view (let’s say Foo) into itself and multiple Foo.BarView, Foo.BazView etc. modules. While that helped a bit, we didn’t give enough care to abstracting the details out of the views, and so they contained a lot of business logic. We’re now in the process of refactoring it into this pattern:

module Foo.BazView ...
...

type alias Model a =
  { a
    | thingICareAbout : ...
    , anotherThing : ...
  }

type alias Config item msg =
  { isLoading : Bool
  , toggle : item -> msg
  , removeAll : msg
  , ...
  }

view : Config item msg -> Model a -> Html msg
view config model =
  -- all business logic gets delegated to functions given by `config`

Seems to work a bit better, ie. business logic moves back into Foo, and the views are “pure”.


Note your problems with the 1300-line file might just be about effective navigation in your editor. I can’t speak for editors like VSCode or IntelliJ IDEA – in those you can probably utilize stuff like go to definition, find all usages, etc. – but I use Vim so I had to learn stuff like /^view :<Enter> to get to a definition quick, or * and n or Shift+n to move between occurences of a variable name. So I don’t particularly care about order of definitions in a file, or the number of them. So while I don’t know how experienced you are, I suggest getting to know your editor more in depth might help alleviate those pains a bit!

2 Likes

As a general rule I now avoid splitting stuff into modules without very good reason.

I have a scaffold for complex apps. that looks like this:

/Data
    Common.elm 
    Foo.elm 
/Pages
    Home.elm
    Bar.elm
/Widgets
    Common.elm
    SomeComplexWidget.elm 
Api.elm
Main.elm
Ports.elm
Route.elm

The philosophy of this structure is inspired by The Elements of User Experience.

I isolate all the Business Objects related functionality (declaration, decoders, encoders, custom accessors, toString) into each object’s Data file.

The rest of the code ends up in each page’s file. Some of the more complex widgets end up in their own file in the Widgets folder.

Api.elm acts as an adapter for the API of the backend.

In extremely rare cases, I feel the need to extract a section of the code of a page into its own module. It would end up in a module like Pages.Bar.SectionOne (/Pages/Bar/SectionOne.elm)

Thank you - I will take a look at the videos.

Thought use of hotkeys etc can help with the navigation, I still find it messy with that large of files. Of course, it might also be related to me being used to other languages where the focus is more on splitting the code, especially separating the views and the functionality.

I have tried separating the views out (I think that would be a huge relief for me), but encountered circular dependencies issues. My temporary solution is to not use the definition of the input parameters in the functions, but that also increase the risk of errors (and I would assume is a bad way of solving things).

This is close to what I have done, by extracting some of the heavier stuff into a separate file, but then sometimes encountering circular dependencies issues (as described in my reply above). It might be that I should take a look at the page itself, and either break it down into smaller and independent modules, or also into separate pages, given that there is alot of functionality for one page

You can break out the circular dependencies to a new module that both original modules will depend on. Maybe Data.Foo in @pdamoc’s scaffold is a good example?

If you split the business objects in their own module, sometimes a lot of the code goes away (big records definitions, decoders, encoders).

Then there are complex widgets that need not know about the actual Msg of the page. You can split them into their own module and use a Config record to give them the needed messages or message creators. This makes the widgets completely independent of the rest of the code.

If you do this, the code remaining into the page module becomes much more manageable and there is little chance of circular dependencies.

I follow same way:

Data/ … domain data specific structures + related functions what work on that data + encoders + decoders + toStrings + parsers
Pages/ … big chunks of pages that are changing are reacting to url + init + update + view per page
Controls / … for stuff that is always there (think discourse top menu) + init + update + view
Elements / … I eek on word widget … :slight_smile:
… rest is the same structure but different names.

Api is Network
Route is Routes
and there are Flags that spec flags and decoder for it

Hello!

Heres generally how I organize things:

Main.elm
Top level, init, update, view, and subscriptions

Model.elm
Main Model, plus helper functions for working with it. For the most part, this is just Page + Session

Session.elm
Session is state that persists between page changes. Things like User, Time, Random.Seed

Msg.elm
Main Msg type, plus maybe some decoders for decoding port Msgs, or key stroke Msgs

Data/*.elm
Modules with one data type plus helper functions

View/*.elm
Modules that are for things that simply make Html msg. Examples: Button, Input, Checkbox

Ui/*.elm
Modules that are for big things that make Html msg, and probably have their own Msg type, update or state. Examples : Modal, Dropdown, Field.

Service/*.elm
Modules for complex, effect-ful, and sometimes stateful processes that dont have a view. Examples: Api,

Page/*.elm
Just the pages of the app

Flags.elm
Route.elm
Ports.elm

I find that generally my modules either come as view / Msg / update or Type / helper functions bundles, and that the Type / helper functions modules never import view / Msg / update modules.

3 Likes

I’ve posted this before, but it’s still exactly how we structure code almost three years later, so this is very “battle tested” and it works great.

2 Likes

Another good video worth watching if you haven’t yet is

I have already taken a look at it, but might do again

Thank you, I will take a look!

Where do you put models, messages etc more specific to a specific Page? In a file under Page?

I use modules mostly to draw boundaries between functionality.

When main gets annoying to work with, I usually start to extract the parts that belong together into separate modules and sometimes change the API to be more readable.

  • I actively avoid any kind of init/update/model/view/subscriptions-esque module-separation. Any given module might have all of these functions, but the less I need the better.
  • I try to only expose functions that take either 1 or 2 arguments. If more is needed, a record is great to make it more readable.

There has been discussion about “components” where people assumed it would be good to start new modules with all of init/update/view etc. This is rarely valid at first, because it introduces very strict separation and unnecessary boilerplate. Some modules might grow into this, and that might be the most maintainable approach in some cases, but don’t start there.

Happy coding :sunny:

1 Like

Msg
My Msg types are always in the same module as either the view or update they accompany. So if there was a Page/Home.elm, the Home.Msg type is in Home.elm.

My thoughts are that Data/X.elm are useful because putting the X type its own module lets it (0) be imported into many places without conflict, and it also (1) creates a standard set of functions for operating on X. But Msg types dont get imported widely like point (0) and they dont need to be manipulated like in point (1), so dont really fit the bill of a Data/X.elm module.

Model
For page Models I dont have an as clear cut answer. Generally yes. But I often find it useful to put Models inside their own module, like Page/X/Model.elm and treat it much like I would for a Data/X.elm module. Also, I avoid making things stateful as long as I can. Pages are generally pretty big, so they usually warrant their own Model type, but for as long as I can I would avoid doing so.

1 Like

We try to start with one module per Widget where we put everything.

When this becomes too big. First thing we move out are the models and the data fetching to a module like WidgetX/Data.elm. We use GraphQl, so having shared models across the app doesn’t work at all, e.g. having a /Data/User.elm wouldn’t work well, as this model will be different in every widget.

Moving the models and commands to another module is usually easy as it doesn’t create circular dependecies (for a command we pass the message constructor to the function).

Usually we don’t split the views, but for some big widgets we do. This is usually the place where we end up with circular dependecies, in this case we put the models and messages in /WidgetX/Shared.elm.

So we don’t have a consistent approach accross all widget, but it doesn’t bother us. We have three structure patterns in our app:

  • Everything in one. e.g. WidgetA.elm
  • Only models and Cmds out e.g. WidgetA.elm, WidgetA/Data.elm
  • Multiple view modules e.g. WidgetA.elm, WidgetA/Shared.elm, WidgetA/Shared.elm, WidgetA/Head.elm, WidgetA/Body.elmt where we put everything

If I split out a child page that needs its own Model, Msg, update and view and possibly also init and subscriptions, I put all of those things in a single file for the child page. Maybe something like this:

Main.elm   # has the top-level Model, Msg, update, view, init, subscriptions
/Pages
-- ChildPage.elm    # has the child specific Model, Msg, update, view, init, subscriptions
-- AnotherChildPage.elm

But you don’t always have to reach for this nested pattern as I am sure you have read elsewhere in this thread - try it out and compare with flatter application structures like elm-taco-donut and get a feel for what you think works best.

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