Once your website or application goes past a small number of lines, it will inevitably contain bugs of some sort. This isn’t specific to JavaScript but is shared by nearly all languages—it’s very tricky, if not impossible, to thoroughly rule out the chance of any bugs in your application. However, that doesn’t mean we can’t take precautions by coding in a way that lessens our vulnerability to bugs.
Pure and impure functions
A pure function is defined as one that doesn’t depend on or modify variables outside of its scope. That’s a bit of a mouthful, so let’s dive into some code for a more practical example.
Take this function that calculates whether a user’s mouse is on the left-hand side of a page, and logs true if it is and false otherwise. In reality your function would probably be more complex and do more work, but this example does a great job of demonstrating:
function mouseOnLeftSide(mouseX) {
return mouseX < window.innerWidth / 2;
}
document.onmousemove = function(e) {
console.log(mouseOnLeftSide(e.pageX));
};
mouseOnLeftSide()
takes an X coordinate and checks to see if it’s less than half the window width—which would place it on the left side. However, mouseOnLeftSide()
is not a pure function. We know this because within the body of the function, it refers to a value that it wasn’t explicitly given:
return mouseX < window.innerWidth / 2;
The function is given mouseX, but not window.innerWidth. This means the function is reaching out to access data it wasn’t given, and hence it’s not pure.
The problem with impure functions
You might ask why this is an issue—this piece of code works just fine and does the job expected of it. Imagine that you get a bug report from a user that when the window is less than 500 pixels wide the function is incorrect. How do you test this? You’ve got two options:
- You could manually test by loading up your browser and moving your mouse around until you’ve found the problem.
- You could write some unit tests (Rebecca Murphey’s Writing Testable JavaScript is a great introduction) to not only track down the bug, but also ensure that it doesn’t happen again.
Keen to have a test in place to avoid this bug recurring, we pick the second option and get writing. Now we face a new problem, though: how do we set up our test correctly? We know we need to set up our test with the window width set to less than 500 pixels, but how? The function relies on window.innerWidth, and making sure that’s at a particular value is going to be a pain.
Benefits of pure functions
Simpler testing
With that issue of how to test in mind, imagine we’d instead written the code like so:
function mouseOnLeftSide(mouseX, windowWidth) {
return mouseX < windowWidth / 2;
}
document.onmousemove = function(e) {
console.log(mouseOnLeftSide(e.pageX, window.innerWidth));
};
The key difference here is that mouseOnLeftSide()
now takes two arguments: the mouse X position and the window width. This means that mouseOnLeftSide()
is now a pure function; all the data it needs it is explicitly given as inputs and it never has to reach out to access any data.
In terms of functionality, it’s identical to our previous example, but we’ve dramatically improved its maintainability and testability. Now we don’t have to hack around to fake window.innerWidth for any tests, but instead just call mouseOnLeftSide()
with the exact arguments we need:
mouseOnLeftSide(5, 499) // ensure it works with width < 500
Self-documenting
Besides being easier to test, pure functions have other characteristics that make them worth using whenever possible. By their very nature, pure functions are self-documenting. If you know that a function doesn’t reach out of its scope to get data, you know the only data it can possibly touch is passed in as arguments. Consider the following function definition:
function mouseOnLeftSide(mouseX, windowWidth)
You know that this function deals with two pieces of data, and if the arguments are well named it should be clear what they are. We all have to deal with the pain of revisiting code that’s lain untouched for six months, and being able to regain familiarity with it quickly is a key skill.
Avoiding globals in functions
The problem of global variables is well documented in JavaScript—the language makes it trivial to store data globally where all functions can access it. This is a common source of bugs, too, because anything could have changed the value of a global variable, and hence the function could now behave differently.
An additional property of pure functions is referential transparency. This is a rather complex term with a simple meaning: given the same inputs, the output is always the same. Going back to mouseOnLeftSide
, let’s look at the first definition we had:
function mouseOnLeftSide(mouseX) {
return mouseX < window.innerWidth / 2;
}
This function is not referentially transparent. I could call it with the input 5 multiple times, resize the window between calls, and the result would be different every time. This is a slightly contrived example, but functions that return different values even when their inputs are the same are always harder to work with. Reasoning about them is harder because you can’t guarantee their behavior. For the same reason, testing is trickier, because you don’t have full control over the data the function needs.
On the other hand, our improved mouseOnLeftSide
function is referentially transparent because all its data comes from inputs and it never reaches outside itself:
function mouseOnLeftSide(mouseX, windowWidth) {
return mouseX < windowWidth / 2;
}
You get referential transparency for free when following the rule of declaring all your data as inputs, and by doing this you eliminate an entire class of bugs around side effects and functions acting unexpectedly. If you have full control over the data, you can hunt down and replicate bugs much more quickly and reliably without chancing the lottery of global variables that could interfere.
Choosing which functions to make pure
It’s impossible to have pure functions consistently—there will always be a time when you need to reach out and fetch data, the most common example of which is reaching into the DOM to grab a specific element to interact with. It’s a fact of JavaScript that you’ll have to do this, and you shouldn’t feel bad about reaching outside of your function. Instead, carefully consider if there is a way to structure your code so that impure functions can be isolated. Prevent them from having broad effects throughout your codebase, and try to use pure functions whenever appropriate.
Let’s take a look at the code below, which grabs an element from the DOM and changes its background color to red:
function changeElementToRed() {
var foo = document.getElementById('foo');
foo.style.backgroundColor = "red";
}
changeElementToRed();
There are two problems with this piece of code, both solvable by transitioning to a pure function:
- This function is not reusable at all; it’s directly tied to a specific DOM element. If we wanted to reuse it to change a different element, we couldn’t.
- This function is hard to test because it’s not pure. To test it, we would have to create an element with a specific ID rather than any generic element.
Given the two points above, I would rewrite this function to:
function changeElementToRed(elem) {
elem.style.backgroundColor = "red";
}
function changeFooToRed() {
var foo = document.getElementById('foo');
changeElementToRed(foo);
}
changeFooToRed();
We’ve now changed changeElementToRed()
to not be tied to a specific DOM element and to be more generic. At the same time, we’ve made it pure, bringing us all the benefits discussed previously.
It’s important to note, though, that I’ve still got some impure code—changeFooToRed()
is impure. You can never avoid this, but it’s about spotting opportunities where turning a function pure would increase its readability, reusability, and testability. By keeping the places where you’re impure to a minimum and creating as many pure, reusable functions as you can, you’ll save yourself a huge amount of pain in the future and write better code.
Conclusion
“Pure functions,” “side effects,” and “referential transparency” are terms usually associated with purely functional languages, but that doesn’t mean we can’t take the principles and apply them to our JavaScript, too. By being mindful of these principles and applying them wisely when your code could benefit from them you’ll gain more reliable, self-documenting codebases that are easier to work with and that break less often. I encourage you to keep this in mind next time you’re writing new code, or even revisiting some existing code. It will take some time to get used to these ideas, but soon you’ll find yourself applying them without even thinking about it. Your fellow developers and your future self will thank you.
via planetweb
0 commentaires:
Enregistrer un commentaire