Most language tutorials start with language features: here are numbers, here are strings, in a way ever so slightly different from other languages. Not this one. I’m hunting the big game.
What I hope to do here is to walk you through the big ideas of Elm, with reference to what you already know from JS, without getting dragged down by what you actually type. There will be no code examples.
This isn’t meant to help you decide if you want to try Elm. It’s best if you have either made the leap and are asking “now what?”, or if you want you know what all the fuss is about.
The other major shared feature is function scope, including closures. You can define local constants (including functions) inside a function. These values are visible even when the parent function returns, if it returns a function that refers to those values. We’ll see in a bit why it’s very common to return a function.
The first of these inabilities is immutable data: values never change once defined. Instead you create copies of the values with your changes applied. (It’s done in a way that is less computationally expensive than you might think.) Second, stateless functions (or pure functions) are easier to debug and test than those than can read or write some global state. Except there is no global state, because that would be mutation. And if you could rebind a variable from one stateless function to another, it wouldn’t seem stateless. See how these ideas connect?
Your immutable data follow a unidirectional data flow. React showed that this was a good idea, and Angular abandoned bidirectional flow in version 2. (Redux is actually influenced by Elm, not the other way around.) Ember makes a point of templates sending actions back to their controller, not updating the values directly. And so in Elm, you receive messages at the “top” of your program and they propagate downward to define a new model, and commands. Commands are a form of managed effects, the fourth major idea. Instead of doing something like setting a timeout or making an HTTP request, you create a description of the work to be done. Like a function that returns a Promise, this description does nothing until you run it, or in Elm’s case, hand it off to the runtime to be run.
Because Ramda.js is a thing that exists, you may have heard of “currying”. I prefer the term “partial application” since it succinctly describes the idea of passing a function some but not all of its arguments, so that it returns another function. You can set this up either manually or with a library in JS, but it’s clunky. In Elm, every function is partially apply-able by default, and it’s something you find yourself using without much ceremony. One thing to be aware of is that Elm functions do not support optional arguments, since omitting a argument means partial application. This downside comes up in practice a lot less than you might think.
If you’ve worked with Redux, you’ve had to make sure that many parts of the program agree on the string constant that names an action. More than that, you need to ensure that every one of those names is used by a reducer, somewhere. Elm solves this problem, and many more, with union types. You can define new constants that are all values of a new type, and the only values of that type. Then you can use pattern matching (like a switch statement) to handle each of these constants, and the compiler will make sure you got them all.
But union types are even more flexible than that, because each value doesn’t have to be a constant identifier. It can also carry values of other types, that is, it can be a function from one or more values to the new type. When you pattern match, you have access to those values. As a concrete example, the standard Maybe a type can be “just” a value (of type a, i.e. whatever type you like) or “nothing”. This allows Elm to handle nullable values very explicitly to avoid bugs. Functions that don’t accept Maybes a guaranteed to be passed a value of the right type, and not undefined.
Immutability means that objects in memory can only hold a reference to objects older than they are. Therefore a lot of functional data structures are tree-like. Graphs typically involve some form of IDs. Dictionaries and sets are implemented as binary search trees, and therefore their keys must be comparable.
At some point, you will need to conditionally render some piece of content, i.e. only show some snippet of html if such and such is true. So the template language will need something that looks like an if statement. Or perhaps its a large number of conditions all revolving around a single value. So the template language will need something akin to a switch.
Then you will likely need to present some lists or tables. So the template language will need some kind of looping construct.
Eventually your templates will grow large. So you’ll need some way organize them, break them down into smaller pieces. Some way for one template to call another template. Some way to define reusable chunks of template code. The template language will need constructs for all of this.
And then you’ll need some way to format numbers and dates.
And so on.
Over time, most template languages eventually reinvent practically every feature of a general purpose programming language.
And that’s what template languages really are. They are programming languages. Usually bad programming languages.
Elm has no separate templating language. It does not even have special syntax for templates. Instead it offers every HTML tag as a function of two arguments: a list of attributes (classes, event handlers, href, etc.) and a list of children (HTML elements themselves, recursively, or a text node). Although you’ll often see these lists written as literals with square brackets, you can substitute any list function such as mapping for them. And instead of needing special components, helpers, and partials, you can extract helper functions in your views wherever it makes sense to do so, just as you would any other code.
Another major difference you’ll encounter compared to JS SPA frameworks is that there is no magic as to when your code is called. You will not have code called because it is named similarly to some other piece of code, or because it extends a provided class with mysterious behavior, or because a lifecycle hook got called. You will not have code called when you did not expect, with an inconsistent state, and no idea how to debug it. In Elm, there are only a handful of “high up” functions (primarily update and view) that are called by the runtime. All other code to run is called, eventually, by those functions. The Elm programmer has complete control over the call stack.