I’ve been using Elm on and off for a few years and I’m about the start a small/medium sized app (probably end up around 10k-20k loc).
I’m toying with the idea of keeping the entire application in one giant file for several reasons:
Abstraction to separate modules always produces a ton of boilerplate and indirection (in my experience)
Everything can be scoped in a file.
I’m curious how it’ll play out (miserable failure or epiphany!)
I’m curious if anyone else has experience with really big files and how they felt about it.
I think the biggest issue is going to be editor support for navigation and performance. I’ve been a vim user of many years so I’m looking into other options that are Vim-like (neovim, and the rust ones). Would appreciate any suggestions.
I feel like the longer I can go without splitting, the more productive I’ll be.
P.S. entirely solo project, so not worried about annoying other developers with my experiment.
Yup, I’ve seen that. I’m going to see what happens when I take that to the extreme. Suspect I will split out my API & decoders, but hopefully that’s it.
It’s an okay idea, I think in a file as big as 10k lines, internal organization becomes important.
Having different sections and jumping between them quickly is a key feature.
Vim has a feature where the file is folded into different sections on opening and marks to jump between them.
With a large file, some editors can become slow or just not work especially after adding language features via LSP.
Lastly you will have to rely on opening the same file in two tabs on different view ports so that you can look at two related pieces of code together.
I agree. Cold folding and a mini map will be essential, both of which I can do with vim already. I really like the code outline view in Onivim 2, so it’s something I might check out.
Having the same file open multiple times is as simple as splits in vim, and something I already do.
Editor performance is definitely a concern I have. So far I haven’t had any issues with large files in vim, so I hope it’s a non-issue.
The Elm compilation cache invalidates at, and compiles at, the module level as the smallest unit, so in your case you’ll be forcing Elm to re-compile (parse, lex, canonicalize, type infer, type check, constrain) your “entire project” each time.
That said, the Elm compiler is bloody fast, so I’m curious if it’ll matter much at 10k or 20k… keen to hear how it goes for you!
This is a really fun experiment! I’m particularly curious to hear if you felt the need for opaque types on this project. I’m also curious namespacing functions will work out in a 10K file (prefix? large let…in?).
Yes, large let scoping basically should cover all my needs in theory. Maybe there’s a future where editor navigation is so good files / modules only matter for libraries?
For other projects derived from this, I split the code into multiple files and it became much simpler to organize the app and jump to the relevant piece of code.
Nice example. I’ve gone up to 3k loc myself in one elm file. It became hard to navigate, but overall felt just fine (once I suspended my intuitions from previous dev work). One thing I noticed though is that this happened accidentally. I would have organized and scoped / localized much differently had it been intentional (convenience functions at the root level that really should have been nested in a scope).
The only pain I recall though was the editor experience. I was thinking a hierarchical minimap / outline could be really awesome for this. Picture your typical project tree of files, but repurposed for functions / scope. Scrolling and opening closing necessary leafs in the hierarchy very quickly would have mitigated most if not all of the pain (filesystem after all is just an organized hierarchy).
It’s actually kind of funny how we’ll break out view functions at the root level that are only called from one function (no reuse) and could totally be scoped (or even unnecessary except for the readability of naming). For example the viewReadme in the elm packing site’s source is an example of this, package.elm-lang.org/Docs.elm at master · elm/package.elm-lang.org · GitHub is never called outside of viewContent and yet it’s at the root scope. Seems totally fine to me, but also only done this way for readability / traditional editor experience. I wonder what I’ll end up with if all of my functions are at their most narrow scope. Honestly could end up being a mess, but I want build up a big codebase though just to see if it really proves to be terrible. I’m a bit worried about horizontal line size, especially with the elm convention of 4 space indentation.
P.S. your section block comments are crazy! Very original.
I think this is fine and a totally legitimate way of doing things and it will be interesting to hear how you get on. As othes have noted, editor support for navigating around the file will be critical.
It will also be intersting to know how you go about splitting into smaller files at a later time, if you decide to do that. It is tempting to default into spliting out into ‘components’ - that is units of (Model, Msg, init, update, subscriptions, view) and using Cmd.map, Sub.map and Html.map to combine them. This is often a mistake and certainly should not always be assumed to be the best way to go. I think keeping everything in one big file to begin with might help you see more clearly what the best way of splitting into smaller files is, since you will not make that choice prematurely.
I am very interested to hear how you get on. For a long time I’ve felt that many developers think that “modularisation is a good thing” and “splitting my project into multiple files is modularising it” therefore it’s a good thing. However, I often feel that splitting a project into multiple files makes more permanent the initial modularisation that you came up with in the beginning. Of course you’re still able to change that modularisation but I feel it happens way less often because the separate files has somehow made it more difficult to change/refactor the modular organisation.
A few comments:
Elm does not have in-file modular constructs - this means you don’t get the kind of ‘formal prefixes’ that come with modules. For example, I prefer AboutUs.view to viewAboutUs, though I cannot say exactly why. As well as (as mentioned above) opaque types.
Elm does not allow mutually recursive modules. I find that this can ‘breed module breakouts’. So I often start with a single file for a project, but at some point I feel like I want to split out, say the rendering of a particular page. To do that though, I have to split out at least the Msg type (unless the page in question doesn’t use any messages). Or maybe I have a separate sub-message type for messages that that page can return, but that’s usually not possible (because it can still return some generic message that is used on other pages). Anyway I find I’m sometimes forced into splitting factoring out more than I originally intended to.
The way a single file project plays with source code control is interesting. There are pros and cons, for example in a multiple-file project you can quickly see which files a commit has touched in a way that obviously doesn’t quite work for a single file project. I guess the more general point here is that it’s not just ‘Elm the language’ and ‘Elm the compiler’ that is effected by this choice, most obviously the editor is effected in ways discussed above.
I think if everything is in a single file is the default, then it’s a useful “taking stock” moment when you decide that you wish to break something out into its own module. It’s useful at that point to consider “should I be breaking this out into its own project”. I don’t have any evidence for this claim.
Was just thinking about this and its worth noting that splitting out Msg into its own file, to avoid circular dependencies, is not the only way.
For example, this view function:
view : Html Msg
view =
div
[ onInput GotInput ]
[ ... ]
Can be moved to its own module without depending on Msg:
view : (String -> msg) -> Html msg
view inputTagger =
div
[ onInput inputTagger ]
[ ... ]
You could event define a set of all the event handlers you need as a record, to avoid having lots of function parameters in the situation where you need many event handlers:
type alias EffectBuilders msg =
{ onInput : String -> msg
, onMouseOver : msg
, ...
}
Not necessarily recomending this, but its worth knowing about.
Modules are largely just groupings of functions and types so having them all in one group in a giant file or grouping them in to multiple files doesn’t involve additional boilerplate or indirection. It’s just namespaces.
The mess people get in to is when they start trying to make their modules completely independent of the rest of their app. This is fine if you want to make something that is reusable in a library in other apps, but it’s overkill when you’re not doing that.
You definitely want multiple modules, 20k long files will be a pain to work with. But you probably don’t need multiple Msg types.