Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62
Subscribe to my email list now at http://jauyeung.net/subscribe/
Functional programming is a programming paradigm which states that we create computation as the evaluation of functions and avoid changing state and mutable data.
In JavaScript, we can apply these principles to make our programs more robust and create fewer bugs.
In this article, we’ll look at how to apple some functional programming principles in our JavaScript programs, including pure functions and creating immutable objects.
Pure Functions
Pure functions are functions that always return the same output given the same set of inputs.
We should use pure functions as much as possible because they’re easier to test and the output is always predictable given a set of inputs.
Also, pure functions don’t create any side effects, which means that it does change anything outside the function.
An example of a pure function would be the following add
function:
const add = (a, b) => a + b;
This function is a pure function because if we pass in 2 numbers, we’ll get the 2 numbers added together returned.
It doesn’t do anything outside the function, and it doesn’t have values that change the returned value inside the function.
Example of functions that aren’t pure functions include:
let x;
const setX = val => x = val;
setX
isn’t a pure function because it sets the value of x
which is outside the function. This is called a side effect.
A side effect is a state change that’s observed outside the called function other than its returned value. setX
sets a state outside the function, so it commits a side effect.
Another example would be the following:
const getCurrentDatePlusMs = (milliseconds) => +new Date() + milliseconds;
getCurrentDatePlusMs
returns a value that depends on +new Date()
which always changes, so we can check the value of it with tests easily. It’s also hard to predict the return value given the input since it always changes.
getCurrentDatePlusMs
isn’t a pure function and it’s also a pain to maintain and test because of its ever-changing return value.
To make it pure, we should put the Date
object outside as follows:
const getCurrentDatePlusMs = (date, milliseconds) => +date + milliseconds;
That way, given the same date and milliseconds, we’ll always get the same results.
Immutability
Immutability means that a piece of data can’t be changed once it’s defined.
In JavaScript, primitive values are immutable, this includes numbers, booleans, strings, etc.
However, there’s no way to define an immutable object in JavaScript. This means that we have to be careful with them.
We have the const
keyword, which should create a constant by judging from the name of it, but it doesn’t. We can’t reassign new values to it, and we can’t redefine a constant with the same name.
However, the object assigned to const
is still mutable. We can change its properties’ values, and we can remove existing properties. Array values can be added or removed.
This means that we need some other way to make objects immutable.
Fortunately, JavaScript has the Object.freeze
method to make objects immutable. It prevents property descriptors of an object from being modified. Also, it prevents properties from being deleted with the delete
keyword.
Once an object is frozen, it can’t be undone. To make it mutable again, we have to copy the object to another variable.
It applies these changes to the top level of an object. So if our object has nesting, then it won’t be applied to the nested objects.
We can define a simple recursive function to do freeze level of an object:
const deepFreeze = (obj) => {
for (let prop of Object.keys(obj)) {
if (typeof obj[prop] === 'object') {
Object.freeze(obj[prop]);
deepFreeze(obj[prop]);
}
}
}
The function above goes through every level of an object and calls Object.freeze
on it. This means it makes the whole nested object immutable.
We can also make a copy of an object before manipulating it. To do this, we can use the spread operator, which works with objects since ES2018.
For example, we can write the following function to make a deep copy of an object:
const deepCopy = (obj, copiedObj) => {
if (!copiedObj) {
copiedObj = {};
}
for (let prop of Object.keys(obj)) {
copiedObj = {
...copiedObj,
...obj
};
if (typeof obj[prop] === 'object' && !copiedObj[prop]) {
copiedObj = {
...copiedObj,
...obj[prop]
};
deepCopy(obj[prop], copiedObj);
}
}
return copiedObj;
}
In the code above, we create a new copiedObj
object to hold the properties of the copied object if it doesn’t exist yet, which shouldn’t in the first call.
Then we loop through each property of the obj
object and then apply the spread operator to copiedObj
and obj
to make a copy of the values at a given level. Then we do this recursively at every level until we went through every level, then we return the final copiedObj
.
We only apply the spread operator if the property doesn’t exist at that level yet.
How do we know that this works? First, we can check if properties of each value are the same, which it is if we run console.log
on it. Then we can also check with the ===
if the copied object has the same reference as the original object.
For example, if we have:
const obj = {
foo: {
bar: {
bar: 1
},
a: 2
}
};
Then we can check by writing:
const result = deepCopy(obj);
console.log(result);
To check the content of result
.
We should get:
{
"foo": {
"bar": {
"bar": 1
},
"a": 2
}
}
This is the same as obj
. If we run:
console.log(result === obj);
we get false
, which means the 2 aren’t referencing the same object. This means that they’re a copy of each other.
Pure functions and immutability are important parts of functional programming. These concepts are popular for a reason. They make outputs predictable and make accidental state changes harder.
Pure functions are predictable since they return the same output for the same set of inputs.
Immutable objects are good because it prevents accidental changes. Primitive values in JavaScript are immutable, but objects are not. We can freeze it with the Object.freeze
method to prevent changes to it and we can also make a copy of it recursively with the spread operator.