Injecting for Purity
Functional Programming is based on pure functions, which have several good qualities that make for better code. However, usual practices don’t normally apply them, with consequent difficulties and problems. In this article we’ll explain what pure functions are, why they matter, and how to use injection, a functional programming concept, to work with frameworks such as React.
Pure vs Impure functions
What are pure functions, and why should we care about them? The key concepts are understandability and maintainability. Pure functions behave just like mathematical functions in two important senses:
- Given the same arguments, pure functions always return the same result.
- Pure functions do not produce any observable side effects when they do their work.
A function that behaves in this way is predictable, testable, and easier to follow and understand. Before studying how to achieve this style, let’s see some examples of impure functions. After, we’ll consider how to make them better.
An example of impurity
Let’s start with a basic example of impure functions. A function that calculates the square root of a number should (obviously!) always produce the same result for the same number! The following example shows a different case: isAdult()
is meant to check if a person is an adult by testing if he’s 21 in 2021.
currentYear = 2000;
function isAdult(yearBorn) {
return yearBorn - currentYear >= 21;
}
The result of the function (which certainly isn’t very sophisticated in the way it calculates ages, I admit it) doesn’t only depend on its yearBorn
parameter, but also on an external value, currentYear
. If you modify the value of that variable, isAdult(...)
will start returning different results. The function is not pure by our definition, and not very safe to use either! A much better version would be as follows.
function isAdult(currentYear, yearBorn) {
return yearBorn - currentYear >= 21;
}
This is a simple problem, and the solution is easy. Instead of the function directly accessing an external value, we provide it to control what the function uses. A similar case would occur in React if a functional component directly accesses a Redux store; we’ll see more about this below. However, there are more complex cases to attend to.
More complex impurities
The situation becomes more complex if the function does some “side jobs” and changes external things. Even if the function always returns the same result, it may cause other problems down the line. Ideally, functions should work with nothing but the arguments they are called with. Functions shouldn’t access other things (like global variables as in our original isAdult(...)
example) and they should just calculate something without modifying anything else. This meshes well with the “single responsibility” design principle that basically says that a function should do one thing, just that thing, and nothing but that thing.
Another type of side effect is when a function modifies its parameters. JavaScript passes arrays and objects as references, so if a function modifies them, it affects the original argument. This is not helped by several methods that do the same: for instance, array.sort(...)
modifies the array in place, instead of returning a new one. Other array methods are even trickier: array.pop()
and array.shift()
both remove and return an element of the array, and the latter is modified. This is not ideal, but at least is expected — but why should users of your function expect it to modify its arguments?
Functions that don’t have side effects are simpler to reason about: you don’t have to consider any external aspects to use them. Also, working with such functions is safer: you don’t have to worry about them messing with something they shouldn’t. Imagine what would happen if the isAdult(...)
function itself modified currentYear
! When you call a pure function, you don’t have to worry about something unexpected happening.
Also, you can refactor and rewrite a pure function, keeping it pure, and it won’t break other things. Finally, doing unit tests for pure functions is much easier; another plus. However, as we’ll see, total purity is not achievable, and we’ll have to compromise; let’s see this.
The need for impurity
What are side effects? One category is when the program interacts with outside entities: the user, a database, another computer, a remote service. The second category is when code changes the global state: modifying some global variable or mutating an object or array received as an argument, for example. Note that side effects aren’t some sort of “collateral damage”: with our definition, something as trivial as logging to the console is considered a side effect — even if you totally planned to do so!
However, if you decided to forego all side effects, your programs would be truly uninteresting! They wouldn’t be able to receive inputs, access files or databases, or even show whatever they calculated! Your web pages would also suffer: the user wouldn’t be allowed to enter values, web service calls would be forbidden, and so would be updating the DOM.
So, the idea of reducing side effects sounds good but becomes too shackling. We have to accept that working with impure functions is a must and that our code will have to interact with users, services, files, and more. However, we have to find some way to contain such functions, so we can limit their scope and ensure they don’t do the unexpected. How can we alleviate the possible problems? In the next two sections we’ll consider the most common problems: doing I/O and working with state.
Dealing with impurity: injecting functionality
If you work with React or other similar frameworks, it’s fairly common to have components calling to APIs to get data.
In React you’d probably go with the useEffect hook, but that isn’t relevant now.
This is a common pattern, but it makes testing harder and doesn’t really allow black-box testing. To write mocks for, say, Jest, you have to be aware of how the API is called, and that requires looking into the code. In true black-box testing fashion, you shouldn’t have to be aware of internal details of a component to test it: you shouldn’t even have to know if (and how) it calls an API.
import React, { useState } from "react";
import ReactDOM from "react-dom";
const UserEmail = () => {
const [email, setEmail] = useState(null);
React.useEffect(
() =>
fetch("https://randomuser.me/api/")
.then((result) => result.json())
.then((data) => setEmail(data.results[0].email)),
[]
);
return <span>Random email: {email ?? "(loading...)"}</span>;
};
ReactDOM.render(<UserEmail />, document.getElementById("root"));
In our UserEmail
component, we are using fetch
to call a random user generator API (in more realistic cases the call would depend on other parameters and user input). We can free the component from the dependency on an API by injecting a prop that will do the I/O. This can be said to be an example of the Strategy design pattern, common in OOP (Object Oriented Programming). The new definition of the UserEmail
component would be as follows.
const UserEmail = ({ getData }) => {
const [email, setEmail] = useState(null);
React.useEffect(() => getData().then((value) => setEmail(value)), []);
return <span>Random email: {email ?? "(loading...)"}</span>;
};
The needed function to be injected would be something like this.
const getEmail = () =>
fetch("https://randomuser.me/api/")
.then((result) => result.json())
.then((data) => data.results[0].email);
ReactDOM.render(
<UserEmail getData={getEmail} />,
document.getElementById("root")
);
With this new implementation, testing becomes easier (it would be even better if the component just displayed an email received as a prop, and didn’t try to get it by itself, but we’ll let that pass for now). You don’t have to know internal details of the component; it’s enough to know that getData
is a function that returns a Promise that resolves to an email. To test this component you’d render it providing a function and verifying if the output correctly shows the fake email.
const getEmail = () => new Promise((resolve) => resolve("fake@email.com"));
Injecting the needed functions in this way makes for clarity; now the component is “dumber” and, despite having side effects, they are controlled by the component’s parents. You could change the way you get an email, and the component wouldn’t require any changes, and all existing tests would still work.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Dealing with impurity: handling state
The second problem with our functions: how to manage state? State is, by definition, a global set of values, and it implies the possibility that any function or component may modify it. Tools such as Redux offer a way to manage it in a more guarded way (because the only way to update state is through a limited set of actions) but the fact remains that any component could modify the store that keeps global state.
It’s been said that with old GOTO-based coding, the big question was “How did we get here?” and with modern style programming the question is “How did state become thus?”
Redux allows your component to directly get data without “prop drilling” at the cost of taking us back to having the equivalent of a global variable. Accessing data is a problem (of the same type that we illustrated in our isAdult(...)
example) but we can cope by passing down props… but what about updating data? Injection can help us there too.
Imagine in our UserEmail
component that we wanted to store the email address in the store. You could have code like the following — and yes, a much better practice would be having separate concerns in separate files, but I wanted to make the example simple! First, let’s have a store.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
const rootReducer = (state = { email: null }, action) => {
if (action.type === "SET_EMAIL") {
return { ...state, email: action.payload };
} else {
return state;
}
};
const store = createStore(rootReducer);
// continues...
Our component could update the store in the following way, by dispatching an action. Once again, in actual React+Redux applications, you would probably use action creators, but that’s not needed and working like this reduces boilerplate.
// ...continued
const UserEmail = ({ getData }) => {
const [email, setEmail] = useState(null);
React.useEffect(() => {
getData().then((value) => setEmail(value));
store.dispatch({ type: "SET_EMAIL", payload: email });
}, []);
return <span>Random email: {email ?? "(loading...)"}</span>;
};
Our component now dispatches and action to update the email in the store. But what if you wanted to use this component without updating state — just to show results, but not store them? Or, if you wanted to write tests for it without much problem? The solution is, again, to inject a function: in this case, one that will take care of the data update. Let’s add a setData prop to the component.
const UserEmail = ({ getData, setData }) => {
const [email, setEmail] = useState(null);
React.useEffect(() => {
getData().then((value) => {
setEmail(value);
setData(value);
});
}, []);
return <span>Random email: {email ?? "(loading...)"}</span>;
};
Now, if you want your component to store data, you could use it as follows.
const setEmail = (email) =>
store.dispatch({ type: "SET_EMAIL", payload: email });
ReactDOM.render(
<Provider store={store}>
<UserEmail getData={getEmail} setData={setEmail} />
</Provider>,
document.getElementById("root")
);
Now we totally control what the component will or won’t do. If we don’t want it to update the store, we should pass an empty function that won’t do anything — or better yet, define a default value for setData
.
const UserEmail = ({ getData, setData = () => {} }) => {
.
.
.
};
For testing purposes, you would provide a mock function and then you could test if it was called with the right parameters; easy!
Summary
Aiming for pure functions is a great goal — though some flexibility is required! Using injection can help you write clearer and more easily tested code. With this practice, you may even allow higher degrees of functionality by externally defining how components will work. Getting accustomed to injection may take a bit, but the results are worth it; give it a try!