AppDevRocks

January 10, 2019 in

Intro to Functional Programming

What is Functional Programming?

Functional programming is a style of programming where you avoid mutating (or changing) data directly, but instead create new data. The key to functional programming (and the reasons it's called "functional" programming) are functions, which when given specific input always produce the same output. To put it simply, the goal of functional programming is really to take something complex, and break it down into easily readable and easily testable code.

In this article we're going to look at the following concepts which will hopefully give you a foundational understanding of functional programming, and empower you to start using these in your code:

  • What pure functions are.
  • How to split code into readable and reusable functions.
  • What Map and Reduce are.
  • Why immutable data matters.
  • The benefits of functional programming.

Note: even though the following code examples are in JavaScript, these same concepts will apply to functional programming in any language.

What are "Pure" Functions?

You may have heard the term "pure functions" before, but what are they exactly? A pure function is a function which takes 1 or more arguments, returns a value, does not mutate (or change) any data, and also is not affected by anything outside of the function.

Here's a simple example of a pure function:

const multiplyBy10 = number => number * 10;

It takes a single number argument, and returns that value multiplied by 10. So, if we call this function and pass in a value of 5, it will always return 50. It doesn't matter if a radio button is checked, the user's permissions, or if it's Saturday, this function will always return the same result when you feed it the same value.

You'll probably run into some resistance as you start working towards functional programming. For example, say you have an array of cars, and you want to remove the last one. Normally, your first through might be something like this:

let cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];

cars.pop();

console.log(cars); // [ 'BMW', 'Toyota', 'Lexus' ];

The problem here is that the cars variable has been modified directly, breaking the rule of "no side-effects". Instead of thinking "I need to remove an element from this array" try thinking "I need to create a new array which doesn't contain the element". Let's look at a functional approach:

const cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];

const filteredCars = cars.splice(-1, 1);

console.log(cars); // [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];
console.log(filteredCars); // [ 'BMW', 'Toyota', 'Lexus' ];

The difference here is that the original cars variable is untouched, and instead we have a new variable filteredCars which contains the value we need. This might seem trite, but consider this function:

// a function which removes the last element, 
// ...and returns the updated array
const removeLastItem = items => {
	items.pop();
	return cars;
}

// define our array of cars
const cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];

// call our function
const filteredCars = removeLastItem(cars);

// check our arrays
console.log(filteredCars); // [ 'BMW', 'Toyota', 'Lexus' ];
console.log(cars); // [ 'BMW', 'Toyota', 'Lexus' ]; // huh?

This function seems straight forward enough. It takes our array of cars, removes the last element using pop(), and we return our new array. Why does our cars array end up modified then? The problem is that because arrays are passed by reference instead of value, so the function is actually modifying our original array when we call pop(), not just a copy of the value.

Now, watch what happens if we take what we learned previously and apply it here:

// a function which removes the last element, 
// ...and returns the updated array
const removeLastItem = items => {
	return items.splice(-1, 1); // remember, this doesn't modify the array
}

// define our array of cars
const cars = [ 'BMW', 'Toyota', 'Lexus', 'Honda' ];

// call our function
const filteredCars = removeLastItem(cars);

// check our arrays
console.log(filteredCars); // [ 'BMW', 'Toyota', 'Lexus' ];
console.log(cars); // [ 'BMW', 'Toyota', 'Lexus', 'Honda' ]; // much better!

Since the splice() method returns a new array, instead of modifying the original, our removeLastCar function is now "pure". This makes the code much easier to test and to read. When you look at the code above, you don't need to examine what removeLastCar is doing internally or wonder if it is affecting some other part of the application. Since it is pure, you only need to know that it is returning a new value, and you can proceed to see how that new value is used.

Getting in the habit of using const instead of let can help force you to think in a functional way.

Breaking Down Functions

In order to have success with the functional programming approach, I recommend breaking down complex functions into a series of simpler ones. This not only produces more code reuse, but the code is much easier to read. Let's look at this code:

const isUsernameValid = username => {
	// sanitize user input
	// validate format
	// check if username is available
	// return boolean result
}

if (isUsernameValid("my_cool_username")) {
	// ...
}

Since isUsernameValid is packed with so much logic (even in this simplified example) this increases the chances of an unintended side-effect and ultimately makes this function harder to unit test and debug. Let's break this logic down into separate functions:

const sanitizeInput = value => {
	// return sanitized value
}
const isValidFormat = value => {
	// return true if format is valid
}
const isUsernameAvailable = value => {
	// return true if username is available
}

const cleanUsername = sanitizeInput("my_cool_username");
if (isValidFormat(cleanUsername) && isUsernameAvailable(cleanUsername)) {
	// ...
}

Now the logic has been broken into smaller, and more reusable functions. Additionally, because each function essentially has one main job, unit testing and debugging these are a snap.

Understanding Map and Reduce

Two key concepts in functional programming are "map" and "reduce". As abstract concepts they can be tricky to grasp, but I find that using the following analogy makes it much simpler to understand:

Map reduce analogy

Using the image above as reference, imagine you're making a salad.

  1. Start off with an array of whole vegetables; lettuce, tomato and onion.
  2. Use .map(slice) to apply a slice function (imagine this will slice the provided veggie) to each veggie in the array. The lettuce is sliced, the tomato, and then the onion.
  3. We now have a new array of sliced vegetables.
  4. Use .reduce(mix) to apply a mix function (imagine this function takes a veggie and mixes it with any previous veggie accumulated). The lettuce is fist, then the tomato slices are mixed in, and finally the onions. reduce returns a single, new value.
  5. We now have a single value, which is our salad.

Readability Benefits

In traditional JavaScript you might have some code like this:

let numbers = [1, 2, 3, 4, 5, 6];

for (var i = 0; i < numbers.length; i++) {
  numbers[i] += 1;
}

When you look at this example, you first have to take some time to understand what it is doing. At the top you can see it defines an array of numbers. Then, when you examine the parts of the for loop definition to determine that it is iterating through each item in our numbers array. Finally, inside the for loop, by examining the code you'll find that it's increasing each value by 1. Now, granted this isn't a complicated example, but it still requires you to mentally evaluate the code in order to understand it's intent.

Now, compare that to this code:

const numbers = [1, 2, 3, 4, 5, 6];

const addOne = number => number + 1;

numbers.map(addOne);

First, we have the same numbers array declaration at the top, nothing new there. Next, you'll see we've taken the logic which was in our for loop (increasing each value by 1) and moved it into a new function with a very clear name "addOne". Finally, by using .map() we iterate over each element in the array.

Notice that instead of the for loop with all of it's boilerplate and the math operation, we've replaced it with a line of code that reads much more clearly:

  1. numbers: start with our numbers array
  2. .map(): iterate though each item of the array
  3. addOne: add one to each item

By using a functional approach, with clearly named functions, you end up with self-documenting code; even without comments, the code basically explains itself.

Why Immutability? (use const)

By using .map() in our example above, we end up returning a brand new array of the results, instead of modifying (or mutating) the original array. The benefit of this is that you won't have to dig through code to see if numbers was modified. Imagine you have code like this:

let numbers = [1, 2, 3, 4, 5, 6];

validateNumbers(numbers); // does this modify numbers?
filterNumbers(numbers); // ...or does this one?
doSomethingElse(numbers); // ...maybe this one?

console.log(numbers); // what's the value of numbers?

The problem here is that without digging into the function definitions, we can't tell if the value of numbers has been modified. Additionally, even if the value is the same, there's no guarantee that will be the case under different conditions. Look at an even simpler example:

let numbers = [1, 2, 3, 4, 5, 6];

doSomethingUnrelated();
doSomethingElseUnrelated();
numbers = getOddNumbers(numbers);
doSomethingUnrelatedAgain();

console.log(numbers); // what's the value of numbers?

Notice that you still need to scan through the code to find that one line which changes the value.

Compare that to this:

const numbers = [1, 2, 3, 4, 5, 6];

doSomethingUnrelated();
doSomethingElseUnrelated();
const oddNumbers = getOddNumbers(numbers);
doSomethingUnrelatedAgain();

console.log(numbers); // we know that numbers hasn't changed
console.log(oddNumbers); // ...and this is our modified array

Even with the noise of the unrelated code, we still have confidence that numbers hasn't changed and filteredNumbers contains our modified values. Using const allows you to work with variables with confidence (in most cases) of their values, without having to mentally parse through the code, or add unnecessary comments.

Unit Testing Benefits

If you're not doing unit testing yet, you really should be, especially for code which is shared throughout your application (e.g. helper functions). Plus, when you work with functional programming, unit testing becomes as easy as it gets. Let's see a quick example with our removeLastItem function:

const removeLastItem = items => {
    return items.splice(-1, 1); // remember, this doesn't modify the array
};

// some simple test cases
removeLastItem([ 1, 2, 3, 4, 5 ]) == [ 1, 2, 3, 4 ]; // true
removeLastItem([ 1, 2, 3 ]) == [ 1, 2 ]; // true

Since the function doesn't have any side-effects (i.e. getting/setting values outside of the function itself) we can avoid complicated test scenario setups, or using spies to catch function calls. We know that with a given input, this function will always produce the same output.

Ready to go Functional?

By this point my hope is that you have a better understanding of functional programming and are ready to start applying in your applications. As you look at your existing code, or while putting together the structure for something new, remember these foundational concepts:

  • Write pure functions (no side-effects).
  • Split code into readable and reusable functions.
  • Keep your data immutable; generate new data instead.

Now go forth and write clearer, more testable code.


Continue the Discussion

© 2020 Nick Gagne. All rights reserved.