Is Elm the Right Choice for Your Team?
Elm is a functional programming language, with a focus on the front-end of web applications. It is famous for providing a delightful developer experience, while also providing many useful constraints which allow tooling to optimize it in a variety of ways. These constraints which the language imposes are viewed as either impractical roadblocks or incredible multipliers, depending on the viewpoint of the team involved.
What Sorts of Constraints?
The most obvious constraint in Elm is immutability. There is no way to modify a value in-place, only to create a new copy of it with modifications. This brings with it the superpower of clarity, making it obvious where modifications have happened. In order to achieve this superpower, developers relinquish their rights to make changes anywhere they see fit.
Purity is another major constraint. This is sometimes described as functions always producing the same output when given the same inputs. Ultimately, this means that there is no global state. If a function needs information, that information will be provided as an argument, and if a function needs to produce an effect, that effect will be part of the value it returns. Similar to immutability, purity provides clarity when tracking down behavior, at the expense of some increased verbosity and refactoring journeys when a piece of information is needed deep down a call stack.
Types as a constraint are less foreign to developers who have been increasingly adopting TypeScript over untyped JavaScript. Elm’s type system is stronger than TypeScript’s and also does not include escape hatches like as any or other sorts of casting. While it’s still possible to write an Elm program that just passes around untyped strings, most Elm packages will push developers towards using structured types that make impossible states impossible to represent. Working in this style requires developers to account for every possible scenario, so that there is confidence that the success scenario truly represents success.
Elm values simplicity and explicitness, so Elm does not support type classes or metaprogramming. This means that the delightful (and mysterious) “magic” of the Ruby ecosystem is not available. Frameworks that build on top of Elm (e.g. elm-land and elm-pages) typically do some amount of code generation, because the compiler does not provide tools to magically plug all of the pieces together. In this case, the cost of simplicity and explicitness is a need to embrace boilerplate and/or code generation.
Teams That Will Struggle With Elm
While many teams will, given time, come to appreciate the benefits that these constraints provide, some teams will see the downsides as an obstacle that cannot be (or is not worth being) overcome.
The first prerequisite to enjoying Elm’s tradeoffs is a willingness (perhaps an eagerness) to be uncomfortable while unlearning skills that have served them well but don’t transfer to a functional paradigm. For developers who haven’t had significant experience with functional programming (e.g. developers from an object-oriented and/or imperative background), the shift to solving problems with data modeling and sometimes a touch of recursion will require the ability to embrace a new approach. Most new developers on our team are comfortable with Elm after a week or two of concentrated experience, but there have been some who struggled more long-term with the functional approach.
In general, teams which prioritize the freedom of dynamic languages may not appreciate the benefits that Elm’s constraints provide. Teams which prefer JavaScript over TypeScript may not want to utilize all that Elm’s type system can do1. Teams which avoid strict linting rules may not appreciate the requirements of Elm’s compiler2.
On the other end of the spectrum, teams which are all-in on functional programming may reject Elm’s simple and explicit approach (or its uncompromising purity). Languages like Haskell and Purescript include a wide variety of features for metaprogramming which are unavailable in Elm. Other frontend functional languages, such as ClojureScript, Reason, and ReScript include foreign-functional interfaces (FFI), allowing direct integration of JavaScript with otherwise pure, functional code. Elm rejects the convenience of the FFI approach so that there’s no asterisk required for its guarantees3.
Teams who embrace new technologies and tools rapidly will need to look to other parts of their stack, because Elm values stability over change for its own sake. Enterprise teams that need support for private package repositories, expensive support contracts, or a premium experience in every code editor may find that Elm isn’t the right ecosystem to meet their needs.
Who Does Elm Work For?
Teams that value fast feedback from tooling, who turn up their linting settings to the strictest configuration, or who embrace their language’s type system will probably be delighted by Elm’s design and approach. Teams with a learning culture are often already familiar with many of the trickier ideas in functional programming4. Teams who are pragmatic about delivering value quickly and reliably, by virtue of a development environment they can trust, will likely love Elm’s guarantees and down-to-earth approach5. Teams which embrace “boring” but stable technologies will appreciate Elm’s deliberate cadence. Teams valuing small assets and high performance will love the optimizations Elm’s compiler and tooling ecosystem enable. Teams looking for ways to tame frontend mess may find that the guarantees provided by Elm’s design provide a solid foundation upon which to build an application with a simple design that supports complex use cases without becoming complicated.
Elm is a welcoming but decidedly niche ecosystem. It’s not for everyone and that’s okay. For those teams where it’s a good fit, it can be a dramatic multiplier, delivering unparalleled stability and maintainability, without sacrificing developer experience or performance.
Footnotes
-
However, Elm’s types are much simpler while being more robust than TypeScript’s, so what many teams dislike about TypeScript won’t be an issue with Elm types (since TypeScript is adding types to an existing language and ecosystem, it didn’t have the luxury of designing a sound type system that can provide true guarantees, but instead needs to support the full range of possibilities that untyped JavaScript can produce). ↩
-
However, Elm’s compiler and elm-review’s rules are much more consistent and able to avoid false positives and false negatives in ways that are impossible with other languages, especially JavaScript. ↩
-
This is not to say that Elm isn’t able to interact with the wider JavaScript ecosystem or new browser APIs. It’s only that the interactions are mediated in a way that maintains Elm’s purity. The primary ways to introduce JavaScript integration are via custom elements or ports, Elm’s message passing architecture. The Elm Guide has a page dedicated to understanding the limits of its JS interop story and helping teams decide whether it will meet their needs. ↩
-
Even developers that haven’t explicitly dabbled in functional programming languages will probably find that many of the concepts are familiar. Mainstream languages have been borrowing functional concepts for years. For example, C#’s LINQ operators are all built upon functional assumptions like immutability and purity. ↩
-
Unlike many functional languages, Elm doesn’t expect, much less require, developers to learn about monads and functors to be productive. While category theory is present in the inspiration for Elm’s design, even developers of Elm packages don’t need to be directly familiar with those concepts, they just need to follow the example of other packages in the Elm ecosystem, which comes relatively naturally after a bit of exposure. ↩