How do you organize large Elm files?

After reading @brian’s recent blog post Should Elm Files be Long or Short? I got to wondering – how do different teams organize large files? Do you have written conventions on, say, the order of types, type aliases, functions, etc? How do you encourage people not to reorganize files to suit some personal notion of “right”?

4 Likes

I tend to organize my files in 3 ways:

  1. Level of abstraction
  2. Type
  3. Functionality

These aren’t mutually exclusive and can work in harmony together. Below are some examples from a recent elm gamejam project showcasing some of these approaches in practice. I’ve written a post here about lessons learned that may also be interesting.

Level of abstraction

I tend to organize my files from most abstract to least abstract. If I call out to another function then it will be defined lower down. That way I can scan things by looking at the top of the file and only need to go down if I care about implementation details.

This works really well with my approach to writing view code where I try to define a bunch of domain-specific helpers. For example the following code:

view : Model -> Html Msg
view model =
    case model of
        Loading ->
            loadingView

        Playing state ->
            playingView state

        Lost state ->
            lostView state


loadingView : Html Msg
loadingView =
  -- ...


playingView : GameState -> Html Msg
playingView state =
    Html.main_ []
        [ header state.energyHistory
        , gameBoard state.grid
        , controls state
        ]


lostView : GameState -> Html Msg
lostView state =
  -- ...


-- definitions for header, gameBoard, and controls go down below

By type

If a file contains multiple type definitions, I tend to group all the functions that act mainly on that type in one place (sorted by level of abstraction :wink: ). This makes it easy to browse and search. It also is easier if I ever want to pull that type into its own separate file eventually.

Notice how a have a separate section for fox types and operations in the game jam project mentioned earlier:

-- in Main.elm

-- FOX


type alias FoxConfig =
    { costOfLiving : Energy
    , birthCost : Energy
    , rabbitNutrition : Energy
    }


initialFoxConfig : FoxConfig
initialFoxConfig =
  -- ...


stepFox : FoxConfig -> Position -> Fox -> Grid -> Generator Grid
stepFox ({ costOfLiving } as foxConfig) position fox grid =
  -- ...

Functionality

Even when working with a single type, I might organize code around functionality, typically broken up by comments. So I might have a section for custom constructors, JSON decoders, random generators, etc. Think of how the docs in the core library break up a module by grouping similar functions under headers.

For example Grid.elm in my previously mentioned gamejam project has sections for constructors, random, operations, queries, and stats:

-- CONSTRUCTORS


fromList : List Cell -> Grid
fromList =
  -- ...

-- [more functions...]


-- RANDOM


generator : Generator Grid
generator =
  -- ...


-- OPERATIONS


move : { from : Position, to : Position } -> Cell -> Grid -> Grid
move { from, to } cell grid =
   -- ...

-- [more functions...]


-- QUERIES


nearbyRabbits : Position -> Grid -> List ( Position, Cell )
nearbyRabbits =
  -- ...

-- [more functions...]


-- STATS

type alias EnergyStat =
    { rabbits : Energy, foxes : Energy }


energyStats : Grid -> EnergyStat
energyStats grid =
  -- ...
5 Likes

I keep the “model” type declarations at the very top, starting with the outermost type, then other “model” type declarations just below.

Types not stored against the model and used only as arguments might go anywhere though.

Beyond that, the order doesn’t matter to me… Maybe because the tooling I use makes it easy to jump directly where I need to go, rather than requiring me to get a sense of how the code is laid out spatially?

3 Likes

Call me crazy, but I like to have my functions sorted alphabetically.

At the beginning of the file are all types (alphabetically) and afterwards all functions in alphabetical order.

This has two reasons:

1.) Easier to search & find something. I don’t like to hit CRTL+F and instead I often just scroll up and down 'till I find the function I’m looking for.

2.) Automatically grouping by type. If I have multiple types in a file, i tend to write the name of the type at the beginning of the function (like carToString, bicycleToString). By sorting them alphabetically, I automatically have all my car functions seperated from my bicycle functions.

Only exception to this rule is, if I’m writing a package. Then I’m usually structuring the file following like documentation.

1 Like

If you’re crazy, you’re not alone :wink:

I tend to order many thing alphabetically in elm files : the constructors of custom types, the properties of records, the branches in pattern matching (with a few exceptions to this one, because sometimes the order matters).

By proceding like this you are not always adding thing to the end of a function or a type : a branch to a pattern matching, a property to the model, a variant to the Msg type… so it reduces the number of conflicts when several persons are working on the same file.

3 Likes

Oh yeah, having a deterministic ordering helps avoid merge conflicts.

I often apply it to records, cases, custom types… but not collections of functions? :thinking: Not sure why, it seems beneficial. I’ll try it out.

1 Like