DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Pure Functions Explained for Humans
Alex Khismatulin
Alex Khismatulin

Posted on

Pure Functions Explained for Humans

Start leveraging pure functions TODAY

First things first: you don't have to write code in a functional style to leverage pure functions.

This powerful tool makes it easier to read, reuse, maintain, and test code. Nobody wants to lose any of these benefits because their code is not functional. And you shouldn't neither. So get known to the concept now to make your code even better, functional or not.


Good news: it is extremely easy to understand and start using pure functions.

A simple definition

A function can be called pure if it returns the same output given the same input every time you call it, doesn't consume or modify other resources internally, and doesn't change its inputs.

Ok, this seems to sound way easier than what we usually see when it comes to pure functions. Now let's break it down and see what each part of this definition means and how those parts are named in the professional lexicon.

Returns the same output given the same input

This one means exactly what it says. Every time we call a function with a constant value, it has to return the same result.


Let's consider 2 examples

We will create addFive and addSomething functions and see how they follow(or don't follow) the rule. But before we move forward, can you guess which one violates the rule and why?

addFive function

const seven = addFive(2); // returns 7
Enter fullscreen mode Exit fullscreen mode

If we have an addFive function, we always expect that addFive(2) would return 7. No matter what happens with the rest of a program, when, or where in the code we call addFive(2), it always gives us 7.

addSomething function

const randomNumber = addSomething(2); // returns a random number
Enter fullscreen mode Exit fullscreen mode

As opposed to addFive, there's the addSomething function. As we can guess from the name, it adds an unknown number to a passed value. So if addSomething(2) call returned 6, we have no guarantee that every time we call addSomething(2) it would return 6. Instead, this will be an arbitrary number that we can't predict at the moment of calling the function unless we know how the internal random number generator works. This function does not guarantee to return the same output given the same input.

What does that mean for us?

At this point, we can definitely tell that addSomething is not a pure function. But we also cannot state that addFive is a pure function yet. To do this, we need to check if it satisfies other conditions.

Doesn't consume or modify other resources internally

To explore this topic, we need to think about how the functions from the above examples would be implemented.

First, our pure function candidate, addFive:

function addFive(number) {
  return number + 5;
}
Enter fullscreen mode Exit fullscreen mode

As we can see, the function does exactly and only what it says and what we expect it to do. Nothing else other than adding 5 a passed number is happening. addFive passes this check.


Now, let's define the addSomething function that is already known as impure:

let callCounter = 0;

function addSomething(number) {
  callCounter = callCounter + 1;
  const isEvenCall = callCounter % 2 === 0;

  if (isEvenCall) {
    return number + 3;
  } else {
    return number + 4;
  }
}

Enter fullscreen mode Exit fullscreen mode

This function has an external variable that stores the number of times the function was called. Then, based on the counter, we check if it's an even call and add 3 if it is, or add 4 if it's not. This call counter is an external state that the addSomething function uses to calculate the results. Such states fall under the definition of side effects.

Side effect is a modification of any external state, consumption of dynamic external values, or anything a function does outside of the work related to calculating the output.

In our case, addSomething modifies and uses callCounter to calculate the final output. This is a side effect. How could we fix addSomething to clean it up from side effects?

If we can't consume or modify an external variable, we need to make it an input:

function addSomething(number, isEvenCall) {
  if (isEvenCall) {
    return number + 3;
  } else {
    return number + 4;
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we control if it's an even or odd call from outside, and our addSomething function becomes pure. Whenever we call it with the same pair of inputs, it would return the same number.

Don't worry if you still don't quite understand what can be a side effect. We will see more examples of side effects a bit later.

Doesn't change its inputs

For this part we need to create the getFullName function:

function getFullName(user) {
  user.firstName = user.firstName[0].toUpperCase() + user.firstName.slice(1).toLowerCase();
  user.lastName = user.lastName[0].toUpperCase() + user.lastName.slice(1).toLowerCase();

  return user.firstName + ' ' + user.lastName;
}
Enter fullscreen mode Exit fullscreen mode

The function takes an object with first and last names. Then it formats these properties in the object so they start with a capital letter and all other letters are lowercased. In the end, the function returns a full name.

If we skip over potential edge cases, our function will return the same output every time we pass an object with the same values. The function doesn't consume or modify any external resources neither and only calculates a full name. So, does that mean it's pure?

No. And here's why.

The object we pass to getFullName is a referential type. When we change its properties inside the function, the changes get reflected in the original object outside the function. In other words, we mutate our inputs.

// note that all letters are lowercased
const user = {
  firstName: 'alex',
  lastName: 'khismatulin'
};


const fullName = getFullName(user); // returns "Alex Khismatulin"

// Logs "Alex Khismatulin", capitalized. Object is modified.
console.log(user.firstName + ' ' + user.lastName);
Enter fullscreen mode Exit fullscreen mode

Even though primitive vs reference types separation sounds complex, in practice, it is not. Spend a few minutes to check it out. There are plenty of good posts on the topic. Tip: add your preferred language to the end of the search query to get more contextual results. Here's an example for JavaScript.

Input mutations are also considered side effects. We change inputs that come from outside, so we're still changing an external resource but in a different way.

"Same" doesn't always mean "equal"

As we just touched referential types, we should also note that even though pure functions always return the same output given the same inputs, this doesn't mean that all inputs and outputs must be equal to each other. That is possible when a function takes or returns a referential type. Look at this example:

function createUser(firstName, lastName) {
  return {
    firstName: firstName,
    lastName: lastName,
  };
}
Enter fullscreen mode Exit fullscreen mode

This function takes first and last names and creates a user object. Every time we pass the same names, we get an object with the same fields and values. But objects returned from different function calls are not equal to one another:

const user1 = createUser('Alex', 'Khismatulin');
const user2 = createUser('Alex', 'Khismatulin');

console.log(user1.firstName === user2.firstName); // true
console.log(user1.lastName === user2.lastName); // true
console.log(user1 === user2); // false, objects are not equal
Enter fullscreen mode Exit fullscreen mode

We see that firstName from user1 is equal to firstName from user2. lastName from user1 is equal to lastName from user2. But user1 is not equal to user2 because they are different object instances.

Even though the objects are not equal, our function is still pure. The same is applied to inputs: they don't have to be literally equal to produce the same output. It's just not a 100% correct word used in the first place.

It's "identical", not "same" or "equal"

The word "identical" describes what we expect from pure functions best. Values such functions take or return don't necessarily have to be equal, but they have to be identical.

Other side effects

So, what can be a side effect? Here are a few examples:

  • Querying or changing external variables and states
  • Mutating inputs
  • DOM interaction
  • Network calls
  • Calling other impure functions

The list goes on and on, but you get the idea. Anything unrelated to computing output or relies on any dynamic values other than inputs is a side effect.

Moreover, console.log is also a side effect! It interacts with the console, thus doing work unrelated to computing an output. No worries, usually console logs have no impact, so this rule is omitted when debugging code.

Final definition

Now, as we have all the pieces of our simple definition uncovered, we a ready to derive a smarter definition of a pure function:

A function can be called pure if it returns identical output given identical input every time it is called and has no side effects.

Awesome! But there's one thing that might've been bugging you while reading.

What should I do when I do need side effects?

Some things are impure by their nature. At the end of the day, this is what programming is about – transforming data is our bread and butter.

Side effects are imminent. But when we have to deal with them, we should strive to isolate them as much as possible and separate from the code that executes pure operations.

Here's a pretty widespread Redux selector pattern. We have a code that gets a snapshot of Redux state and a selector function that knows how to get a specific value from that state:

function getUserById(state, userId) {
  const users = state.users.list || [];
  const user = users.find(user => user.id === userId);
  return user;
}

const state = store.getState();
const user = getUserById(state, '42');
Enter fullscreen mode Exit fullscreen mode

You don't need to know anything about Redux to understand the example. There's no magic going on here. store.getState() in our case only returns an object that holds some values.

In this example, the values in the store change dynamically and are out of our control. We secure the getUserById value selector function from any third-party states and make it only rely on its inputs.

You see the pattern: separate the code that has to deal with impure data sources or to produce side effects from the code that gives linear output based on its inputs.

What are the pros?

Reusability

Let's come back to the Redux selector example. Other than just returning a user from state, we can update the code and break it down into a few pure functions:

function findUserById(list, userId) {
  const user = users.find(user => user.id === userId);
  return user;
}

function getUsersFromState(state) {
  const users = state.users.list || [];
  return users;
}
Enter fullscreen mode Exit fullscreen mode

Now we have one function that knows how to get users from state and another one that knows how to find a user by id in a list of users. That means we can reuse findUserById in other parts of the app where we use the same shape for the user object:

// find a user in the store users
const state = store.getState();
const users = getUsersFromState(state);
const user = findUserById(users, '42');

// find a user in the lottery players list
const lotteryPlayers = getLotteryPlayers();
const winnerId = (Math.random() * 100).toFixed();
const winner = findUserById(users, winnerId);
Enter fullscreen mode Exit fullscreen mode

Both cases leverage findUserById because it does one small thing and has no unpredictable dependencies. If we ever needed to change the field name that holds user id, we would need to do that in just one place.

Purity gives us space to create functions that are not bound to specific data sources or context in which functions are called.

Testing

We're going come back to the Redux selector example one more time and imagine that we decided to get state from the store right inside the selector function:

function getUserById(userId) {
  const state = store.getState();
  const users = state.users.list || [];
  const user = users.find(user => user.id === userId);
  return user;
}

const user = getUserById('42');
Enter fullscreen mode Exit fullscreen mode

What would it cost us to add a test that validates this function? Well, we would need to do some dark magic to mock store.getState():

test('Should return user with correct id', function() {
  store = {
    getState() {
      return {
        users: {
          list: [{ id: '42' }],
        },
      };
    }
  };

  const user = getUserById('42');
  expect(user.id).toBe('42');
});
Enter fullscreen mode Exit fullscreen mode

You see what's going on? We had to mock the whole Redux store just to test one small selector. More importantly, the test must know how the state is retrieved from the store. Imagine what would we need to do to test a more complex one? What would happen if we decided to replace Redux with some other state management tool?

To see the difference, here's a test for the original pure version:

test('Should return user with correct id', function() {
  const state = {
    users: {
      list: [{ id: '42' }],
    },
  };

  const user = getUserById(state, '42');
  expect(user.id).toBe('42');
});
Enter fullscreen mode Exit fullscreen mode

Now we don't need to think about what method is used to return a state from the store and mock the whole thing. We just use a state fixture. If we ever change a state management tool, this will not affect the tests because they only know what the state's shape is, not how it's stored.

They make the code easier to consume

Last but not least, writing pure functions forces us to create smaller, more specialized functions that do one small thing. The code is going to become more organized. This, in turn, will increase readability.

In the end

Pure functions alone are not going to make your code perfect. But this is a must-have part of your toolset if you want to be a professional in what you do. Every little step moves you to a bigger goal, and pure functions are not an exception. Employ this concept and make your code a little better today.

I hope you learned something today. Make this topic a small piece in the strong foundation of your success. Thank you for reading!

P.S.

If you like occasional no-bullshit web shorties, you should definitely drop me a line on Twitter. Feel free to tag me if you want to discuss this article, and I will make sure to join the conversation!

Top comments (1)

Collapse
skonik profile image
Sergey Konik • Edited on

Thank you for the article. Brings some clarity regarding code quality.

Can we consider functions that return new objects with updated properties as pure?
e.g

user = {
   username: "jack",
};

function updateUsername(user, new_username) {
  return {
    ...user,
    username: new_username,
  }
};

user = updateUsername(user, "tester");
// user { username: "tester" }

Enter fullscreen mode Exit fullscreen mode

What do you think?

Head to your account's Settings to...

🌚 Enable dark mode
πŸ”  Change your default font
πŸ“š Adjust your experience level to see more relevant content