JavaScript tutorial: Functional programming in JavaScript

How to embrace immutability, think functionally, and write code that is easier to understand and test

JavaScript tutorial: Functional programming in JavaScript
Thinkstock

JavaScript is a flexible language, allowing for all sorts of styles and programming paradigms. Although this breadth sometimes comes at a cost to readability or consistency across a code base, it also allows developers to express their thoughts in different ways depending on the problem. One of those ways, functional programming, has become more and more popular with JavaScript programmers in recent years.

Functional programming relies on pure functions and avoids shared state and mutable data. It’s a programming style that rose to prominence with languages like Clojure, Scala, and Elm, but you can get into the functional mindset right within JavaScript. The following tips should help you to start writing more functional code and reap the benefits that the functional paradigm has to offer—functions that are easier to reason about, code that is easier to understand, and programs that are easier to test and debug.

The first step is embracing immutability. Ordinarily with JavaScript, you can change pretty much any variable you want. Any variable you declare can be reassigned to any other value, unless you use the const keyword. Even then, if you declare a const and assign it an object, you can change anything you want within the object itself. In functional programming, this is a big no-no. Mutations are considered a nasty side-effect that should be avoided. The issues that arise from mutation typically have to do with assumptions about shared state. For example, take the following code:

let myList = [1, 2, 3];
someFunction(myList);
console.log(myList);

If you’re using the full language functionality of JavaScript, you can make no assumptions about what is going to be logged to the console on line three. Instead you have to dig into the implementation of someFunction to see what it does to myList. But if we stick to the functional paradigm, we know that line three will log [1, 2, 3], because someFunction isn’t allowed to mutate the list we passed in. If we want someFunction to return some processed version of the list, we would have to return a new list with the processed values. The old list would remain the same:

let myList = [1, 2, 3];
let newList = mapToLetters(myList);
console.log(myList); // [1, 2, 3];
console.log(newList); // ['a', 'b', 'c'];

Fully adopting the principle of immutability will present challenges you’ve never had to face. If you stick with it, though, you’ll eliminate a whole class of tricky bugs from your work.

You can do quite a bit to incorporate a mindset of immutability in JavaScript without adding any new libraries. A few simple coding preferences will have you well on your way.

First, use const. This is a language-level feature that will keep you from reassigning a variable to a different value. It won't help you when it comes to mutating the contents of objects or arrays assigned to those values, but it does the trick for primitive values like numbers, strings, and booleans.

Second, learn to use the spread operator. When it comes to making modifications to objects and arrays, adding or changing keys to objects can be a cumbersome task. But with the spread operator (), you have a simple way of doing it.

const myObj = {foo: 1, bar: 2};const alteredObj = {...myObj, bar: 3};
const augmentedObj = {...myObj, baz: 3};

console.log(myObj); // {foo: 1, bar: 2}
console.log(alteredObj); // {foo: 1, bar: 3}
console.log(augmentedObj); // {foo: 1, bar: 2, baz: 3}

The spread operator also works for adding items at the beginning or end of arrays:

const myArray = [ 1, 2, 3 ];
const zeroIndex = [ 0, ...myArray ];
const addOne = [ ...myArray, 4 ];

console.log(myArray); //  [ 1, 2, 3 ]
console.log(zeroIndex); //  [ 0, 1, 2, 3 ]
console.log(addOne); //  [ 1, 2, 3, 4 ]

Third, prefer methods that return new instances. Arrays can be sliced or spliced, but one of them will modify the original array in place and the other will return a new array. When you find cases like this, always prefer methods that return new versions of what you're operating on.

I’ll wrap up this discussion by pointing you to a library that offers a little extra help in handling some cases that are really unwieldy to handle with vanilla JavaScript. For example, let’s say we wanted to remove the third element of a four-element array that is a property on a nested object:

const appState = {
  environment: 'dev',
  config: {
    allowedValues: [ 1, 2, 3, 4 ]
  }
};

const updatedState = {
  ...appState,
  config: {
    ...appState.config,
    allowedValues: [...appState.allowedValues.slice(0, 2), ...appState.allowedValues.slice(3, 4)]
  }
};

It’s ugly and not so fun to abstract something usable across your application. This is where you can incorporate libraries like Immutable.js. Immutable.js will keep you and your team honest and ease the pain of situations like those above. Using Immutable.js and updating nested properties becomes as simple as:

import { fromJS } from 'immutable';

const appState = fromJS({
  environment: 'dev',
  config: {
    background: true,
    allowedValues: [ 1, 2, 3, 4 ]
  }
});

const updatedState = updateIn(
  'config',
  'allowedValues',
  theList => theList.delete(2)
);

Once you start embracing immutability, and begin realizing the benefits you gain from it, you will start to see mutable functions as defect surface area. Got any tips, tricks, or additional libraries that help deal with immutability? Continue the conversation on Twitter: @freethejazz.

Copyright © 2019 IDG Communications, Inc.