close

Day 5: Learning from Elm

In the past year, I’ve spent a lot of time and energy learning, teaching and using the Elm programming language. Elm has exploded in popularity in the last twelve months and has carved out a place for itself in an increasingly crowded front end space.

Elm is adventurous; it’s a strictly typed, functional language that takes inspiration and syntax from Haskell yet is aimed at JavaScript developers looking for a more productive environment. I’ve spent a lot of time on stage talking about Elm and a lot of people tell me that they love the idea but can’t quite progress to Elm in their work. This is totally understandable and although I hope more and more people will be able to use Elm at work, we’re not quite ready to leave JavaScript behind just yet.

That being said, I’ve felt my JavaScript (and other programming work) be influenced by what I’ve learned and enjoyed from Elm and in this post I want to talk about how you can still benefit from Elm even if you’re not working in it every day. It’s for this reason that I’d encourage everyone to play with Elm; you’ll benefit regardless of if you use it every day.

Immutability

In Elm every single value is immutable. That means that it can never change – whenever you want to modify a value you instead create a new value that is the old value, but transformed. This is built into Elm; it’s impossible to ever mutate a value. However, this isn’t the case in JavaScript:

var x = { a: 1 }
x.a = 2
console.log(x) // { a: 2 }

var y = [1, 2, 3]
y[0] = 0
console.log(y) // [0, 2, 3]

Even the new const feature of ES2015 doesn’t prevent us from this; it keeps the variable name bound to the same object, but doesn’t prevent that object being mutated:

const x = { a: 1 }

// not allowed!
// x = { b: 2 } errors

x.a = 2
x.b = 3
console.log(x) // { a: 2, b: 3 }

What we can instead do is use Object.assign, a new function introduced in ES2015, to create copies of objects with certain properties applied to them. Let’s say we have a point with an x and a y, and we want to update y to be a new value. We can use Object.assign to create a new object rather than mutate the original:

const point = { x: 0, y: 1 }
const newPoint = Object.assign({}, point, { y: 2 })
console.log(newPoint) // { x: 0, y : 2 }

Object.assign takes objects and copies the properties from right to left. In this case the object { y: 2 } is merged into the point object, and all of that is then copied onto the first argument, the empty object. It’s important that we pass an empty object as the first argument; if we instead just passed point, that would be mutated:

Object.assign(point, { y: 2 })

That code above will mutate point, which is what we’re trying to avoid.

This however is fairly verbose if you’re doing this a lot throughout your application. There is a proposal for ECMAScript called Object Rest/Spread which adds support for spreading an object using the ... operator. This functionality isn’t certain to make it into a future version of JavaScript, but it’s at Stage 3 of the process which means it’s pretty likely. It’s also a very popular proposal and I’m confident enough that it will make it into JavaScript to mention it in this blog post!

If you’d like to use this syntax now, you can use the Babel plugin to add this feature to your codebase. When we do, the code sample from above becomes:

const point = { x: 0, y: 1 }
const newPoint = { ...point, y: 2 }
console.log(newPoint) // { x: 0, y : 2 }

The advantages of this syntax are:

  • You can never accidentally mutate point, the spread operator doesn’t allow us to do it.
  • It’s much cleaner and more concise than Object.assign.

I use this pattern a lot in my code and I’ve found it a really nice way to avoid random mutations that I wasn’t expecting. If you wanted to go a step further you could introduce something like Immutable.js, a library that gives a whole new set of data types for keeping things immutable, but I personally find that using Object.assign or ... and being careful when working with objects is a nice middle ground. There’s also a benefit to using native data structures rather than ones provided by an external library.

Pure Functions

Another characteristic of Elm is that all its functions are pure. I’ve written extensively about pure functions before but in short a pure function is a function with the following characteristics:

  • All data it works with is given to it as input.
  • It does not mutate any data that it is given.
  • Given the same input, it will always return the same output.

A function is pure if it doesn’t reach out to any global scope to access any data, and if it uses the data it’s given to return some other value without modifying anything along the way. A good way to test is a function is pure is to see if it always gives you the same result when you give it the same input. For example:

function pureFunc(x, y) {  
  return x * 2 + y * 3
}

pureFunc is pure because:

  • All the data it works with it is given (x and y).
  • It does not mutate x and y.
  • It does not reach out into any global scope to access data.
  • If I call it with x = 1 and y = 2 it will always give me back the same answer, 8.

On the other hand, the below function is not pure:

function notPureFunc(x, y) {  
  window.result = x * 2 + y + 3 + z * 4
  return window.result
}

It’s not pure because:

  • It has a side effect, because it modifies window.result.
  • It relies on an external variable, z.
  • If I call it with the same x and y I don’t know that it will return the same value because the value of z could be anything.

There are two main benefits to pure functions in my experience. They are much easier to work with, because you don’t have to remember where z comes from, or deal with any other information outside of the body of the function, but this also makes them much easier to test. By having functions that rely on no external input other than their arguments, you can easily test them because there is no test set up required and no special framework specific configuration to set up before you can get on with testing. I’ve lost track of the amount of times I’ve wanted to test a simple function that’s nested within a framework controller or service, and I’ve had to jump through so many hoops that often I just ended up not testing it because I couldn’t deal with the frustration!

Separation of view and logic

Speaking of frameworks, that brings me nicely onto my next point. Elm does a great job of separating your core logic with your user interface. You should always strive to keep the two very separate as they are two completely different concerns. You should have code that can take your data and render it, and you should have code that can manipulate data based on input and user actions, and the two should very rarely cross.

In Elm we follow the Model-View-Update function, which breaks down into three parts:

  • The model is the single object (or record in Elm parlance) that contains your entire state.
  • The view function can take the current model and render the user interface based on the data it has. It can also bind event handlers to listen for user input, but it cannot modify data in anyway.
  • The update function deals with user actions and uses them to update the model accordingly. When this is done the view function is then run again, so that the output on the screen is representative of the new state.

What this means is that there’s only one function above where data can change – in the update function. That means in order to test your application you only have one real function to test – the update function (as your app gets larger you’ll split it into many functions – but they all remain easy to test). What I enjoy most about this approach is how my tests on the update function never once reference the user interface, or the framework I’m doing. You can of course still test your user interface and that it’s rendered correctly given the data you give it, but the core logic of your application is nicely decoupled from it.

If you think this pattern sounds familiar then you’d be right – libraries such as Redux were influenced by Elm’s model, and it’s something that has now become a much more common approach. I’d highly encourage you to try this in your own applications. How much of the user interface you test is down to you, but I’ve found in Elm, as well as in React, that these days I don’t tend to test my UI much. I trust the framework to do the right thing, and focus on getting my core business logic correct.

Making an effort to avoid wrapping your code in framework boilerplate is also hugely beneficial. Most libraries have the concepts of services or factories that wrap logic up for you, but the cost there is that you now have your code that you want to test abstracted into a framework. Often this is a worthwhile cost and one you’re happy to pay, but often it’s beneficial to just create standalone modules of related logic in plain JavaScript, and import those into your framework specific components.

Functions, functions, functions.

When you follow the above rules and find yourself sticking to plain old JavaScript everywhere instead of wrapping code around a particular framework, you’ll notice that things become much easier to abstract, refactor and work with. You write plain old JavaScript objects (POJOs) that contain pure JavaScript functions, and suddenly everything is much easier to reason about, work with and test. You might even throw a bit of TypeScript or Flow to add some type checking to your code (it’s not quite as nice as Elm’s compile time type checks – but what is?!), and suddenly you’re oozing with confidence as you pull large functions apart, change arguments to other functions and pull logic out of components into modules of business logic detached from your user interface.

The beauty of this approach is that you strip away code and concepts that prevents you from easily reasoning about your system. Your UI components effectively become components that can take data and produce DOM; your application’s modules are just POJOs with functions within, and your functions rely on no outside state. Once everything is in this shape you can test it easily; once things are tested you can refactor with confidence, and once you can refactor with confidence you can maintain your application, and once you can maintain your application as requirements change and libraries mature over time, you’ll be chirpier than ever on your way into the office.

Conclusion

I hope this article has peaked your interest about Elm the programming language but also the ideas and concepts that are at the core of Elm. They can be applied to your Javascript just as readily as to your Elm, Ruby, or any other programming language of your choice. If you’ve got any questions I’d love to hear them, tweeting me is your best bet, and the Elm Guide is the best way to dive in and start exploring Elm.