Web Dev Diaries

A blog and resource for modern web development

Functional Programming with JavaScript

Every now and then, a particular programming paradigm becomes a fad. Usually, this is because a select framework or library pops up and encourages swarms of developers to flock to it. Lately, the trend has been to learn Functional Programming.

If you’re a web developer, you’ve almost certainly heard of React within at least the last year or more. Whilst I’m not going to discuss that library here, I _would_ love to share with you some of the Functional Programming techniques and their benefits that I’ve picked up in the last 12 months of working with React and how they’ve altered the way I write JavaScript apps.

What’s Functional Programming?

These two words have been doing the rounds a lot since React sprung up. Functional Programming is not a new concept by any means, and some programming languages have been built from the ground-up to be as Functional as possible. When you describe a program as being Functional, you’re saying it’s:

  • Pure & without side-effects
  • Easy to reason about
  • Incredibly testable
  • and because of the above, generally less susceptible to bugs

Sound promising? Sound like something you’d love to incorporate into your own JavaScript projects? Let’s get straight into some examples.

Pure, without side-effects

Let’s dive straight into 2 basic functions that achieve a similar result. See if you can guess which one is considered _pure._ Even if you’re unsure what the word _pure_ really means in this context, I’ll bet you can guess:

var data = [1, 2, 3];

function addTwoToEach () {
  var newData = [];
  data.forEach(function (datum) {
    newData.push(datum + 2)
  });

  data = newData;
}
function addTwoToEach (data) {
  return data.map(function (datum) {
    return datum + 2;
  });
}

var data = addTwoToEach([1, 2, 3]);

Spoiler: It’s the second.

Saying a function is pure is the same as saying it’s got no side-effects. Side-effects are the cause of writing functions that rely on and sometimes mutate (change) variables defined outside their scope. Look at the first example – see that variable data we defined above it? It’s being mutated from inside the function addTwoToEach!

Now, look at example 2. In case you don’t know, the array .map() method performs an operation on each array element and returns a new value to put in place of the original array element. However, it doesn’t change the original array at all- it returns a completely new array.

Why does this matter? In these isolated examples, I’ll admit it’s not easy to appreciate which is the better approach. But when you think about adding more functions that may rely on the contents of data, which approach allows you to be more certain of what data contains? The reason it’s the second is because you can clearly see data being set to whatever addTwoToEach returns. There’s simply nothing else that the variable can be equal to here. This is a good reason why the new ES6 JavaScript const is better to use than var because a const can’t be re-assigned later. I’ll cover this in more detail in another post soon I’m sure.

Easy to reason about

So now we’ve seen how if a function is allowed to change variables outside of its own scope, then it enters a strange state wherein we can’t be absolutely sure what our variable will be equal to. To _reason about_ your app is to be able to express your app’s state and intent as succinctly as possible without requiring stacks of comments in our code or other external resources. This is important for two reasons:

  1. We need to be able to revisit our code later and know what’s what
  2. Other developers will need to be able to start working on our code and know what’s what

Writing apps that are easy to reason about is synonymous with writing apps that are easier to debug, easier to make changes to and easier to write tests for.

var data = [1, 2, 3];

function addTwoToEach () {
  var newData = [];
  data.forEach(function (datum) {
    newData.push(datum + 2)
  });

  data = newData;
}

function removeLast () {
  data.pop();
}
function addTwoToEach (data) {
  return data.map(function (datum) {
    return datum + 2;
  });
}

function removeLast (data) {
  return data.filter(function (el, index) {
    return index < (data.length - 1);
  });
}

var data = addTwoToEach([1, 2, 3]); // [2, 4, 6]
var newData = removeLast(data); // [2, 4]

These two examples make it look more difficult to prove that the Functional approach is the better one because JavaScript doesn’t include a built-in method of “popping” the last element of an array without modifying the original. Using the array .filter() method is just one possible approach that requires no additional variables to be defined.

That aside, I hope these demonstrate how example 1 is the less reliable one: whenever either function gets called, the original data variable is modified. This means predicting what data will be is impossible without seeing every single time that addTwoToEach and removeLast get called in your app and the order that they’re called in.

Example 2 can’t modify data, and both functions only return a value based on the parameter that is passed. This means it’s entirely predictable: if we pass x, we know for certain we’ll receive back y.

Incredibly Testable

Testing is essential for professional software projects. Sooner or later you’ll likely need to write tests if you’re working in teams of developers. My advice: start learning sooner than later if you don’t already know how to write tests!

Anyway, let’s check out a couple of new examples and see how we might test them:

var values = [];

function addValue (value) {
  values.push(value);
}

function deleteAtIndex (index) {
  values.splice(index, 1);
}

And here’s a simple test that ensures our functions behave as expected (I’m using Tape’s syntax here).

it('adds a value', function (t) {
  addValue(123);
  t.deepEqual(values, [123], 'adds the expected value');
  t.end();
});

it('deletes a value', function (t) {
  deleteAtIndex(0);
  t.deepEqual(values, [], 'deletes the expected value');
  t.end();
});

Even if you’re new to unit testing, Tape’s wonderfully simple syntax should make it easy to understand.

Here’s the problem though: the above setup defies a fundamental rule about unit testing that applies to all languages: no shared state. Sharing state between unit tests is very bad practice because tests should each run in isolation as much as possible to avoid 1 test polluting another’s outcome. Imagine what would happen if we needed to add another test to check the adding of another value in between these two above tests. The test suite would run through each test in order, adding the first value, then adding another value, and finally reaching our ‘deleted a value’ test, which will fail because it expected the values variable to be an empty array, which is impossible given that the 2 tests executed beforehand each added a value. It’ll do something like this:

addValue(123); // values === [123]
addValue('abc'); // values === [123, 'abc']
deleteAtIndex(0); // values === ['abc'] - Test fails! Expected empty array

You can quickly remedy this issue by changing what ‘deletes a value’ expects values to be equal to, but this isn’t addressing the root problem. Using a different test environment could also be a way out without needing to change your code, for instance the testing suite Mocha allows you to set up a beforeEach method to run before each test to do “cleanup” operations and wipe everything clean for the next test, so this will be good to do before each test: values = [].

There’s another problem I’ve conveniently overlooked too: how will our tests even know where to find the values variable? If we run our tests inside the same file and after defining the values variable, then it’s fine. But tests written for Tape or Mocha or any number of other test libraries expect tests to be placed into their own files. Doing this means a closure is automatically created around each file that’s imported. Therefore, values will be undefined inside our tests!

I don’t know about you, but to me, this all just feels plain wrong. Let’s solve this issue in one swoop by using a purely functional approach:

function addValue (values, value) {
  return values.concat(value);
}

function deleteAtIndex (values, index) {
  return values.filter(function (value, valuesIndex) {
    return valuesIndex !== index;
  });
}

And the accompanying tests:

it('adds a value', function (t) {
  var values = addValue([], 123);
  t.deepEqual(values, [123], 'adds the expected value');
  t.end();
});

it('deletes a value', function (t) {
  var values = deleteAtIndex([123], 0);
  t.deepEqual(values, [], 'deletes the expected value');
  t.end();
});

Now, it doesn’t matter what order our tests are run in, where they’re situated in relation to our function definitions, or which test suite/setup we have in place. Each test should be more than happy to be run in complete isolation without affecting each other’s outcome.

Wrapping up

The main goal of Functional Programming is not necessarily to help you write fewer lines of code but to help reduce the technical complexity in an app overall, as compared to an Imperative Programming approach. It encourages placing logic into small, easily maintainable functions that only rely on what’s directly passed to them and only return a value based on those parameters. Passing the same parameters 1,000 times to the same function at any given time should yield 1,000 returned values that are exactly the same.

The beautiful and detrimental thing about JavaScript is that it allows you to write your code in any way you see fit. The more we use the language, the more we find ways to refine how we can put pieces together to make apps easier to manage, easier to reason about and more scalable to the requirements they’ll face up to. JavaScript isn’t purely functional by design and because of this, the Functional Programming equivalent of the Imperative solution to a problem may require much more thinking, but with any luck, and assuming you understand FP paradigms well enough, the programs you construct will be much easier and more fun to work with.

comments powered by Disqus