Use Pure Functions to understand Functional Programming

Use Pure Functions to understand Functional Programming

About this Series
In this article, you will learn the basic philosophy behind functional programming.

However, there are different ways of looking at it. Each article in this series will explain it differently and will use a different programming language to do so.

Series overview:

Today we will use JavaScript to look at

Pure Functions

Functional Programming is about thinking and using pure functions.

A function is "pure" if it

  • always returns the same output for a given input and

  • has no interactions with anything outside of its scope (no side effects)

Let's look at an example of a pure function.

function subtractPrice(number) { 
  const price = 20

  return number - price
};

And here's a function that has side effects:

function buy(userId) {
  let user = getUserFromBackend(userId);
  user.money -= 20;
  postUserToBackend(userId,user);
  return user;
};

postUserToBackend and getUserFromBackend interact with some backend server. Therefore they are side effects. But what about user.money -= 20?

It depends. If it changes a variable that is being created inside of the function, like in this case, then it's not a side effect.

However, If we create the user outside of the function, then it becomes a side effect.

function subtractPriceFromUser(user) {
  user.money -= 20;
  return;
}

The idea of functional programming is to separate the business logic from the effects.

function buy(userId) {
  return updateUser(userId,withPriceSubstracted)
}

//effects with no buisness logic
function updateUser(userId,fun) {
  const user = fun(getUserFromBackend(userId))
  postUserToBackend(userId,user)
  return user
}

//business logic with no effects
function withPriceSubtracted(user) {
  //return a copy of user
  return { ...user, money = user.money - 20 }
}

The two key observations here are that we combine all effects into one function and that we copy user instead of updating it. We are still doing the same things are before. However, we have now separated the pure part from the effectful part.

And that's functional programming!

Benefits

So why do we use pure functions? One benefit is that pure functions can easily be tested. We don't need a mocking library, a simple assert function is all we need.

assert(withPriceSubtracted(user).money === user.money - 20)

Another aspect is that by limiting the things we are allowed to do, we decrease the cognitive load while programming. We can be sure that a pure function will only do one thing: compute the return value. No asynchronous calls, no store that is being updated, and no changing state outside the function.

Of course, we have to affect the outside world at some point. But by applying functional programming, we try to manage these effects in one spot and only ever trigger them with intent. To achieve this, we have a functional toolkit (one that I will try to slowly explain throughout this series).

JavaScript has a lot of these nifty little tricks that all try to manage side effects. If you are interested in seeing what JavaScript has to offer, I recommend reading Functional Programming in JavaScript by Luis Atencio for a more in-depth look.

If you liked the post, then I'd suggest you subscribe. Follow along as I explain Functional Programming from different angles and in different languages. The next post in this series will be about Pipelines in Python.


Advanced: Randomness with Pure Functions

So here's an interesting question: Is Math.random() pure? Surely it is right? By the definition in this article, a function is pure if it does not interact with anything outside the domain of the function. Randomness doesn't trigger any remote calls, it doesn't change any variables, right? WRONG!

How is randomness implemented on a computer? Computers can't implement real randomness. We can achieve the illusion of randomness by using a Pseudo-Random-Number Generator (PRNG). In its simplicity, such a PRNG looks like this:

function generate(seed) {
  return seed + LARGE_PRIME_NUMBER % ANOTHER_LARGE_PRIME_NUMBER
}

How come, that we never have used a seed before when generating random values? That's because the seed is a global variable, usually instantiated at the start of our program (and without us ever seeing or knowing about it).

let SEED = 42 //or any other initial value

function random() {
  SEED = generate(SEED)
  return SEED
}

So now you might see why randomness is not pure: The seed changes every time we use a random value! So what can we do?

The answer is to create our own PRNG(or use a library) that lets us create seeded generators. We can now generate a random seed and pass it on to our pure function. With that, we can use the PRNG in the pure world without mutating any global variables.

//pure function
function diceGame(initialSeed) {
  let seed = initialSeed
  seed = generateInt(seed)
  const player1 = seed
  seed = generateInt(seed)
  const player2 = seed
  return player1 > player2
}

//effectfull function
function playGame() {
  return diceGame(random())
}

This might seem overly complicated, but actually, it's not. I only visualise the magic that happens behind implementation details. If we have to use randomness, we can either mock the hell out of our tests or do it the proper way and always seed our generator.


And with that, we reached the end of the blog post. If you came so far and still want more, here is one last brain-teaser: Is Date.now() pure? I'd like to read your answer in the comments.