Functional Programming: An Overview of JavaScript Paradigms

Functional programming is a way of creating computer programs by utilizing expressions and functions, while avoiding changes to the original state and data.

Functional programming aims to produce code that is easier to comprehend and less prone to errors by adhering to these principles. This is accomplished by staying away from control flow statements such as (for, while, break, continue, goto), which tend to make code more difficult to follow. Additionally, functional programming encourages the use of pure, predictable functions, which are less likely to contain bugs.

This article will delve into functional programming using JavaScript. We will explore various JavaScript methods and features that make this possible. Finally, we will examine different concepts associated with functional programming and understand why they are so effective.

However, before diving into functional programming, it’s crucial to grasp the distinction between pure and impure functions.

Pure vs. Impure Functions

Pure functions take input and produce a consistent output without causing any side effects in the external environment.

1
const add = (a, b) => a + b;

In this example, add is considered a pure function because, for any given value of a and b, the output will always remain the same.

1
2
const SECRET = 42;  
const getId = (a) => SECRET * a;

Conversely, getId is not a pure function. This is because it relies on the global variable SECRET to calculate the output. If SECRET were to change, the getId function would produce a different result for the same input, making it impure.

1
2
let id_count = 0;
const getId = () => ++id_count;

This function is also impure due to two reasons: (1) it utilizes a non-local variable to compute its output, and (2) it generates a side effect by modifying a variable outside its scope.

getId is impure illustration

This could pose challenges if we needed to debug this code. For instance, we might need to determine the current value of id_count, identify other functions modifying id_count, and check for any functions depending on id_count.

These are precisely the reasons why pure functions are exclusively used in functional programming.

Moreover, pure functions offer the advantage of being parallelizable and memoizable, unlike the previous two functions. This characteristic contributes to creating high-performance code.

The Tenets of Functional Programming

As we’ve seen, functional programming relies on certain fundamental rules:

  1. Avoid data mutation.
  2. Utilize pure functions: these produce consistent output for given inputs and have no side effects.
  3. Employ expressions and declarations.

Adhering to these principles ensures that our code can be considered functional.

Functional Programming in JavaScript

JavaScript inherently provides functions that facilitate functional programming. Some examples include String.prototype.slice, Array.protoype.filter, and Array.prototype.join.

On the other hand, Array.prototype.forEach and Array.prototype.push are considered impure functions.

While one might argue that Array.prototype.forEach isn’t inherently impure by design, it’s important to consider that it’s primarily used for mutating non-local data or causing side effects. Therefore, categorizing it as an impure function is reasonable.

Furthermore, JavaScript offers the const declaration, which aligns perfectly with functional programming principles as it prevents data mutation.

Pure Functions in JavaScript

Let’s examine some of the pure functions (methods) provided by JavaScript:

Filter

As its name implies, this function filters an array.

1
array.filter(condition);

The condition in this case is a function that processes each element of the array and determines whether to retain or discard it, returning a truthy boolean value accordingly.

1
2
3
const filterEven = x => x%2 === 0;  
[1, 2, 3].filter(filterEven);  
// [2]

It’s worth noting that filterEven is a pure function. If it were impure, the entire filter call would also become impure.

Map

The map function applies a given function to each element of an array and generates a new array based on the return values of the function calls.

1
array.map(mapper)

Here, mapper represents a function that takes an array element as input and returns the processed output.

1
2
3
const double = x => 2 * x;  
[1, 2, 3].map(double);  
// [2, 4, 6]

Reduce

reduce condenses an array into a single value.

1
array.reduce(reducer);

reducer is a function that takes the accumulated value and the next array element as input and returns the updated value. This process is repeated for all elements in the array, one after the other.

1
2
3
const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem  
[1, 2, 3].reduce(sum);
// 6
reduce call illustration

Concat

concat appends new items to an existing array, resulting in a new array. Unlike push(), which modifies the original array (making it impure), concat preserves data immutability.

1
2
[1, 2].concat([3, 4])  
// [1, 2, 3, 4]

The same outcome can be achieved using the spread operator.

1
[1, 2, ...[3, 4]]

Object.assign

Object.assign copies values from a given object to a new object. This method is particularly useful in functional programming for creating new objects based on existing ones while maintaining data immutability.

1
2
3
4
5
const obj = {a : 2};  
const newObj = Object.assign({}, obj);  
newObj.a = 3;  
obj.a;  
// 2

With the introduction of ES6, this can also be achieved using the spread operator.

1
const newObj = {...obj};

Creating Your Own Pure Function

We can also define our custom pure functions. For instance, let’s create one that duplicates a string n number of times.

1
2
const duplicate = (str, n) =>  
	n < 1 ? '' : str + duplicate(str, n-1);

This function takes a string and replicates it n times, returning the newly generated string.

1
2
duplicate('hooray!', 3)  
// hooray!hooray!hooray!

Higher-order Functions

Higher-order functions are functions that accept a function as an argument or return a function. They are often used to enhance the functionality of existing functions.

1
2
3
4
5
6
const withLog = (fn) => {  
	return (...args) => {  
		console.log(`calling ${fn.name}`);  
		return fn(...args);  
	};  
};

In this example, we define a higher-order function called withLog that accepts a function and returns a modified function. The returned function logs a message before executing the original function.

1
2
3
4
5
const add = (a, b) => a + b;  
const addWithLogging = withLog(add);  
addWithLogging(3, 4);  
// calling add  
// 7

The withLog HOF can be applied to other functions without conflicts or requiring additional code. This seamless integration demonstrates the elegance of higher-order functions.

1
2
3
4
5
6
const addWithLogging = withLog(add);  
const hype = s => s + '!!!';  
const hypeWithLogging = withLog(hype);  
hypeWithLogging('Sale');  
// calling hype  
// Sale!!!

It’s also possible to invoke it without explicitly defining a combining function.

1
2
3
withLog(hype)('Sale'); 
// calling hype  
// Sale!!!

Currying

Currying involves breaking down a function that takes multiple arguments into a series of nested higher-order functions, each accepting one argument at a time.

Consider the add function:

1
const add = (a, b) => a + b;

Currying this function would involve rewriting it to distribute arguments across multiple levels:

1
2
3
4
5
6
7
const add = a => {
	return b => {
		return a + b;
	};
};
add(3)(4);  
// 7

A key advantage of currying is memoization. With curried functions, we can memoize specific arguments in a function call, allowing their reuse without redundant computation.

1
2
3
4
5
// assume getOffsetNumer() call is expensive
const addOffset = add(getOffsetNumber());
addOffset(4);
// 4 + getOffsetNumber()
addOffset(6);

This approach proves more efficient than repeatedly passing both arguments.

1
2
3
4
// (X) DON"T DO THIS  
add(4, getOffsetNumber());  
add(6, getOffsetNumber());  
add(10, getOffsetNumber());

Furthermore, we can refactor our curried function to be more concise. Since each level of the curried function call consists of a single-line return statement, we can utilize ES6’s arrow functions to simplify it:

1
const add = a => b => a + b;

Composition

In mathematics, composition refers to chaining the output of one function as the input of another to create a combined output. This concept translates seamlessly to functional programming due to the use of pure functions.

To illustrate, let’s define a couple of functions. The first one, range, takes a starting number a and an ending number b and generates an array containing numbers from a to b.

1
const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];

Next, we have the multiply function, which takes an array and multiplies all its elements.

1
const multiply = arr => arr.reduce((p, a) => p * a);

Let’s use these functions together to calculate the factorial of a number:

1
2
3
4
5
const factorial = n => multiply(range(1, n));  
factorial(5);  
// 120  
factorial(6);  
// 720

This factorial calculation function mirrors the mathematical composition principle f(x) = g(h(x)).

Concluding Words

This article explored pure and impure functions, functional programming in general, JavaScript features that support it, and some fundamental functional programming concepts.

Hopefully, this exploration has sparked your interest in functional programming and inspired you to experiment with it in your code. Embracing functional programming can be an enriching learning experience and a significant milestone in your software development journey.

Functional programming is a well-researched and robust approach to writing computer programs. With the introduction of ES6, JavaScript now provides a more robust and enjoyable functional programming experience than ever before.

Licensed under CC BY-NC-SA 4.0