Learning a new programming language seems to follow the same steps every time: install, Hello World, arithmetic, a few cool features, and then you’re own your own. While the guides may offer a few buzzwords at the very beginning of your journey, it’s not easy to see the forest for the trees and realize what this new language does that makes it unique, worth knowing, and worth using.
This is a point that crystalized in my mind only recently. When we were recording Episode 8 of the Elm Town podcast, Murphy Randle and I got off on a tangent about Haskell. He said:
I could not understand really when I was reading about Haskell why, why were all the books that I was reading talking about type classes and Applicative and Functor and all these things right away. Why do I care? And it didn’t click for like a year that that’s kind of what the language is all about. Learning to use the language is learning what those things are.
I’ve also been watching a bunch of Ruby conference talks recently. A common theme is refactoring and growing an application as it ages and accumulates legacy code. These gurus of OOP disseminate their lore of design patterns and code smells meant to guide their followers of when to extract an object, and how. Superficially, Ruby concerns itself with classes, inheritance, objects, and methods. But from what I gather, Ruby is really about the roles objects can play, and dividing responsibilities and knowledge among those roles. It’s about trading dozens or hundreds of lines of procedural code for a collection of coherent, loosely coupled objects and their messages.
All this leads to the question, what does it mean to use Elm? What is the language really about? And what do those principles buy you? It’s possible to learn the syntax and the libraries without really understanding the story they are trying to tell. If they could speak, this is what I think they would say.
Elm enforces a strict separation between the programmer and effects. The effects must be handled carefully, but the rest of the program is much less constrained. (Photo: ESA Astronaut André Kuipers in 2009)
The fundamental principle of writing Elm code is to separate effects from computation, and to use different techniques for each.
All computation is “pure”, meaning what a function returns depends only on its arguments. It’s predictable and easy to understand.
“Effects” are everything else. These can be random number generation (if you’re not managing the seed explicitly), the current time, and of course, anything from the outside world. So network requests, local storage, and the URL are all effects. After all, we have no way to guarantee an HTTP request will return the same thing every time.
Elm separates effects, and makes them slightly harder to use, so that computations are easier to write. The reason this works is that most of your code is computation, not effects. There are a few general strategies that help you write computation code:
Embrace the pipeline. If C is a sequence of thinly-veiled machine instructions, and Ruby is a sequence of messages between objects, Elm code often takes the form of a sequence of transformations. These are often connected by the |> operator. If you need intuition, think of the UNIX pipe. It’s used to pass data from one operation to the next. (Also, it’s optimized by the compiler so it’s no more expensive than parentheses.) Here’s an example:
sanitize : String -> Maybe Floatsanitize token =token|> String.trim|> String.toFloat|> Result.toMaybe
sumOfString : FloatsumOfString =commaSeparatedFloats|> String.split ","|> List.filterMap sanitize|> List.sum
Extract a function. No one likes passing lots of parameters to a function, regardless of language. But in Elm you’re especially pressed to find places where a computation can be split apart cleanly, passing a minimum amount of state. This leads to functions that are coherent: they do one thing well. The same properties make functions testable: there’s no hidden state to mock. Breaking out functions can help decompose algorithms into useful, and possibly reusable, chunks of code.
Model the problem. In Elm, data is dumb, but predictable. (Contrast OOP, where data is hidden and bundled with behavior.) You should aim to explicitly describe what you are working with. Distinct states should be separate. Impossible states should be unrepresentable. Usually this means defining union types, and then using case statements to handle each possible value. You can use type aliases to refer to what a value means (e.g. URL, PersonID, ShoppingCartItem) rather than how it’s represented (e.g. strings, integers, records).
The compiler is your assistant. Use type annotations to specify your intent, because code is read far more often than it is written. Get comfortable with the idea of mapping over almost anything; you’ll discover the other useful patterns and idioms in time. For applications, don’t worry if lots of code knows how the model is defined; if you change it the compiler will tell you what breaks. (For libraries, you should try to hide the implementation to avoid making breaking changes.) No but seriously: Elm’s compiler messages are unlike anything you’ve seen in C-family languages.
That’s a high-level overview of your toolbox for computational code. When the time comes to deal with those pesky effects, everything from pure computation still works. You model a message type explicitly and consume it in a giant case statement. The compiler still gives you useful error messages, you can write functions to manage effects if you really need to, and the pipe operator still works. There’s just one major addition: The Elm Architecture.
TEA, as it’s affectionately known, is a pattern for dealing with effects at the periphery of your program, and every Elm program uses it. If you haven’t read the guide, the gist is that you define a Model type and a Msg (message) type. As messages come in to your app, you update the Model, which gets passed to a view function that creates a virtual DOM (like React). You can also send information to the outside world as well.
You define update, view, and a few other functions for TEA and the Elm runtime calls them as necessary. All the computation — remember, most of your program — is called from one of these functions.
So, maybe the secret to Elm is that there is no secret. You don’t have to learn a lot of abstractions up front, like in Haskell. You don’t have to do it exactly right to avoid disaster in six months, like in Ruby. Elm discourages second-guessing yourself: if the code does what you want it to do, it’s probably fine. It’s easy to change later, so don’t worry about getting it perfect now.
So give it a try. You might be amazed at how well it works.