These are some of the lessons I wish I had learned when I first picked up Elm, before I wrote a bunch of apps that are now more difficult to maintain than they need to be.
Breaking an Elm program up into multiple files just to reduce scrolling does not tend to work out optimally. Evan gave a really cool talk on this called "The life of a file". Files should split organically around data structures, not just to stay short. The reasons we want to keep JavaScript files as short as possible do not apply to Elm.
For example, I've created apps with a file structure like this:
MyApp.elm
MyApp
|- Model.elm
|- Msg.elm
|- Update.elm
|- Subscriptions.elm
+- View.elm
I've also made huge apps with this structure:
MyApp.elm
Surprisingly, the latter was easier to work with. It actually wasn't even close. With "go to symbol" shortcuts in my text editor, there is just no need to split it up, deal with circular dependencies, and import dozens of little bits of the app in other little bits. It is also a royal pain to work on multiple Elm apps that have similar-sounding sub-packages (as in the first example).
So when do I split code into a new file? When I encounter a function that I want to use in multiple apps OR when a set of functions exist solely for the purpose of manipulating a specific data type. My packages always appear organically to solve a problem, never to reduce scrolling.
Repeated often, but so true I will repeat it again. No matter how good you are, there is probably a way you can make your life easier in Elm by defining better types. I know I am not close to mastering them, but excellent talks like Richard Feldman's Making Impossible States Impossible have really helped me.
Take a deep breath. It will be okay!
The subtext of this rule would probably be: Just write features first. I am again parroting "The life of a file". Creating generalized libraries to satisfy the Elm compiler can be deeply enjoyable, like "code golf", and it is easy to get caught up writing functions for the sake of writing functions. Then the requirements will change, and your fancy abstraction will be irrelevant.
When writing Elm code, I start by writing my Model
and Msg
types, my init
, update
, and view
functions, and not much else. If I find myself needing the same code in two places, only then will I split it into its own function. I let helper functions grow organically.
Only at the end of a project, when the requirements have stabilized and commonalities between things have become apparent, do I start generalizing functions that I want to re-use into modules.
Programmers love to create the most generalized solutions possible. I often try to create generic form helpers and similar libraries to use in Elm apps, but Elm is all about special cases. GUI's can get into all kinds of weird, complicated, and confusing states, it IS NOT easy coding, and the entire point of Elm is to make those states clear and force you to handle them. Do not sweat a little copy and paste, because often those things that started similar can end up diverging in completely different directions by the end of the project.
A theory of general relativity of web apps doesn't exist. Creating a good user experience is just plain old elbow grease. Just relax and write the special case code.
Let Elm programs grow organically and learn from what they become, not what you think they should be. The structures that we try to impose on them often come from object-oriented thinking. However, a lot of those structures will actually make life harder in the long run. A lot of perfectly valid coding practices in Elm feel bad to an object-oriented programmer, so I always ask myself: "Is my discomfort because I actually have a problem, or because I wouldn't do it this way in Ruby?"