Why Understand Pure Functions?
After programming for a while, you’ll find that writing clean, maintainable code is quite challenging. One reason is “functions that cause changes affecting the rest of the program,” leading to “unnecessary side effects that make the code unpredictable and hard to understand.” This article will introduce the definition of pure functions and how to use them, so let’s learn how to write clean and pure functions.
What Are Side Effects?
A function that causes changes affecting the rest of the program, aside from performing calculations and returning results, can be said to have “side effects.” Side effects are just a description and do not have a negative meaning, but unnecessary side effects can indeed make the code unpredictable and hard to understand.
How to Avoid Side Effects?
By avoiding side effects, you can achieve writing clean and pure functions. Pay attention to the following common side effects to improve code quality.
Avoid Using Variables Outside the Function
Take the following function that checks if someone is 18 years old as an example. As soon as it uses variables outside the function, it becomes impure, relying on external variables, making the input not fixed! Using variables within the function scope allows it to be a truly independent unit, not relying on external state for calculations.
// ❌ Impure Functionconst myAge = 18;
function isAdult() { return myAge >= 18;}isAdult();
// ✅ Pure Functionfunction isAdult(age) { return age >= 18;}isAdult(myAge);
Treat Function Parameters as Immutable
For example, if Jack
currently has 10
dollars, and he chooses to work
and earns another 10
dollars, the program can work correctly. However, by using the ...
spread operator to create a copy of the data and return it, you can avoid modifying the original data.
const jack = { wallet: 10,};
// ❌ Impure Functionfunction work(person) { person.wallet += 10;}
work(jack);console.log(jack); // { wallet: 20 }
// ✅ Pure Functionfunction work(person) { return { ...person, wallet: person.wallet + 10, };}
console.log(jack); // { wallet: 10 }console.log(work(jack)); // { wallet: 20 }
Many native functions are impure, such as Array.forEach()
, Array.sort()
, Array.push()
, etc., as they manipulate data directly instead of returning a new result. If you want to adhere to the principles of writing pure functions, it’s best to avoid them and instead use pure native functions like Array.map()
, Array.filter()
, Array.reduce()
, etc.
const ageList = [1, 5, 19, 30];
// ❌ Impure Functionlet ageIsAdult = [];ageList.forEach((age) => { if (age >= 18) { ageIsAdult.push(age); }});
// ✅ Pure Functionfunction ageIsAdultPure(age) { return age.filter((age) => age >= 18);}
console.log(ageIsAdult); // [19, 30]console.log(ageIsAdultPure(ageList)); // [19, 30]
Summary
A function that returns the same result when given the same parameters and has no side effects is a pure function.
The reasoning is simple: as long as the input to the function is fixed, the output will also be fixed, which means it won’t affect the rest of the program, making the program easier to maintain. So should we rewrite all functions as pure functions? Of course not! However, we should try to write functions as pure functions whenever possible, because the benefits of pure functions are:
- Cacheability: Fixed inputs and outputs allow functions to cache results, so when the same parameters are input again, the cached result can be used directly without re-executing complex calculations.
- Testability: Fixed inputs and outputs make functions easy to test.
- Portability / Self-documenting: Simple input and output make functions easier to port to other programs without needing additional documentation.
There is no inherent superiority between impure and pure functions; it is merely a difference in problem-solving methods. However, it is clear that add some constraints on how functions are written can greatly enhance code maintainability, and it is worth a try!