Elm Semantics for JavaScript Developers

1,049 reads

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.

I’m also not going to list all of the many things JavaScript has that Elm does not. Hoisting, prototypes, own properties, this, triple equals, and all the other weird stuff doesn’t exist in Elm. You’ll find that its semantics are actually a lot simpler than JavaScript’s!

Bread and Butter

JavaScript deserves credit as the first language to popularize first-class functions. That’s a fancy term that encompasses four specific abilities. Functions can take other functions as arguments. They can also return functions. Functions can be assigned to vari — err, constants — and placed into data structures. Finally, functions can be anonymous, by using a function literal as an expression. You’d expect no less of a “functional” language. (By the way, function declarations are syntactic sugar for assigning an anonymous function to a constant, unlike JavaScript where hoisting makes these two subtly different.)

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.

Best Practices for Data Flow

If you know React/Redux or ngrx, these next concepts will be familiar to you. Those JavaScript frameworks are converging on a set of ideas that together specify how an application is structured, and how data flows through it. Although the ideas are distinct, the JS and Elm communities have both realized that they work really well together. Elm takes these restrictions and makes it impossible to cheat. After all: libraries cannot provide new inabilities.

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.

Even though the JavaScript community is converging on these ideas, they are far from standard. Every Elm package in the library uses immutable data — the same implementation even — which is more than one can say for npm.

On Your Radar

One major feature of Elm we haven’t talked about yet is its static type system. If you’re an Angular dev, you’ve probably used TypeScript. If you’re in the React ecosystem, you’ve at least heard of Flow. And just about everyone is using Babel or Webpack, so the idea of a build phase isn’t nearly as alien to this community as it once was. All that said, Elm’s type system is able to catch a lot more issues than those on top of JavaScript, which is how it delivers on no runtime errors in practice. Elm’s compiler errors are typically far more helpful than “undefined is not a function”.

JavaScript’s module systems — e.g. AMD, CommonJS, RequireJS — have been standardized into ES6 modules, so this area is less fraught with difficulty than it used to be. That said, Elm files correspond directly to modules and it works out of the box. You can control what is exposed directly at the top of the file. You can expose modules (or keep them private) in published libraries.

JavaScript developers are somewhat distrustful of the package ecosystem, for reasons that start with left and end with pad. Semantic versioning is treated as more of an ideal than standard. But in Elm, every package is semantically versioned, and this is enforced by the type system and other tooling. You can safely upgrade without worrying about breaking your code.

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.

New Frontiers

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.

Under the hood, lists are represented as singlely-linked lists (i.e. stacks), and recursion is used to do most of the work that iteration does in JavaScript. Except, you rarely recurse on a list explicitly. Instead, use the functions in the list library to map, filter, and fold (reduce) over a list. These operations were introduced to Array.prototype in ES5, so you should be familiar with them already.

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.

But enough about data structures; let’s talk about views. Ember has handlebars, a template language written in separate files from JavaScript. React has JSX, with new syntax mixed in to JavaScript. Angular and vue also have their own HTML-like syntax. David Ford lays out the problems with template languages:

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.

Collectively, these features make Elm a coherent language, and one much smaller than JavaScript even before you throw half a dozen React ecosystem libraries into the mix. Everything works well with everything else. Everything makes sense.

Tags