DEV Community

Cover image for Unleash JavaScript's Potential with Functional Programming
Jan Hesters
Jan Hesters

Posted on

Unleash JavaScript's Potential with Functional Programming

Too many JavaScript developers have no idea what JavaScript is capable of.

This article will transform the way you write code and unleash your potential as a programmer.

By the end of this article, and the video below, you will be able to read, understand and write code like this:

const curry =
 (f, array = []) =>
 (...args) =>
 (a => (a.length >= f.length ? f(...a) : curry(f, a)))([
 ...array,
 ...args,
 ]);
const add = curry((a, b) => a + b);
const inc = add(1);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const doubleInc = pipe(x => x * 2, inc);
Enter fullscreen mode Exit fullscreen mode


JavaScript is a two paradigm programming language because it supports OOP and FP.
This article is your "step-by-step with no steps skipped" guide to functional programming, so you can use
the language to its full capabilities and write more modular, deterministic and testable code.
It literally goes from primitives all the way to advanced function composition.

Definition

If you look up functional programming you might get lost in academic lingo.
If you're lucky you will find some kind of simple definition like this:

Functional programming is the process of building software by composing functions. Functional programming is declarative rather than imperative. Side-effects are isolated. And application state usually flows through pure functions.

You can relax because this article will explain all of these terms and more. Starting at Primitives.

Primitives

Primitives are all datatypes that can hold only one value at a time. JavaScript has 7 primitive data types.

  • string: Represents text.
  • number: Represents numerical values.
  • boolean: Represents true or false.
  • undefined: Indicates a variable has not been assigned a value.
  • null: Represents an intentional absence of any object value.
  • Symbol: Creates unique identifiers.
  • BigInt: Handles numbers larger than the standard number type can.
// primitives.js
const string = "ReactSquad.io";
const number = 9001;
const boolean = true;
const notThere = undefined;
const weirdUndefined = null;
const noOneUsesThis = Symbol('🤯 ');
const bigInt = 1n;
Enter fullscreen mode Exit fullscreen mode

Composite data types

Composite data types can store collections or combinations of values.

  • Object: A structure that can hold multiple values as named properties.
  • Array: A list-like structure that holds values in a specific order.
  • Map: A collection of keyed data items, similar to an object but with keys of any type and maintains the order of insertions.
  • Set: A collection of unique values. Like an array, but each value can only occur once.
// composite.js
const obj = { key: "value" };
const array = [1, 2, 3, 4, 5];
Enter fullscreen mode Exit fullscreen mode

A Map in JavaScript is a collection of keyed data items that maintains the order of insertions and allows keys of any type.

// map.js
// Creating a new Map
const map = new Map();

// Setting key-value pairs in the Map
map.set('name', 'John');
map.set('age', 30);
map.set(1, 'one');

console.log(map);
// Output: Map(3) {'name' => 'John', 'age' => 30, 1 => 'one'}

// Retrieving a value by key.
console.log(map.get('name')); // Output: John

// Checking if a key exists.
console.log(map.has('age')); // Output: true

// Size of the Map.
console.log(map.size); // Output: 3
Unleash JavaScript's Potential With Functional Programming Article.md 2024-09-30
3 / 29

// Removing a key-value pair.
map.delete(1);

// Clearing all entries.
map.clear();
Enter fullscreen mode Exit fullscreen mode

A Set in JavaScript is a collection of unique values, ensuring that each value appears only once.

// set.js
// Creating a new Set.
const set = new Set();

// Adding values to the Set.
set.add('apple');
set.add('banana');
set.add('apple'); // This will not be added again.

console.log(set);
// Output: Set(2) {'apple', 'banana'}
// Reminder: Sets delete duplicates.

// Checking if a value exists.
console.log(set.has('banana')); // Output: true

// Size of the Set.
console.log(set.size); // Output: 2

// Deleting a value.
set.delete('apple');

// Iterating over Set values.
set.forEach(value => {
 console.log(value);
});

// Clearing all values.
set.clear();
Enter fullscreen mode Exit fullscreen mode

Functions

function sumDeclaration(a, b) {
 return a + b;
}
const sumArrow = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode

A function is a process that can take inputs, called parameters, and can produce some output called return value.

Parameters vs. Arguments

  • Parameters are variables defined in the function declaration. They act as placeholders for the values that a function will operate on.
  • Arguments are the actual values or data passed to the function when it is called. These values replace the parameters during the function's execution.
  • An application happens when the arguments are used to replace the function's parameters. This allows the function to perform its task using the provided arguments.
function myFunction(parameter) {
 return parameter;
}
const myArgument = "Some value";
myFunction(myArgument); // Output: "Some value";
Enter fullscreen mode Exit fullscreen mode

A function can be a mapping, a procedure or handle I/O operations.

  • Mapping: Produce some output based on given inputs. A function maps input values to output values. (So the examples you saw earlier are mappings.)
const square = x => x * x;
square(7); // 49
Enter fullscreen mode Exit fullscreen mode
  • Procedure: A function executes some steps in a sequence. The sequence is known as a procedure, and programming in this style is known as procedural programming.
function prepareTea(teaType) {
 let steps = [];
 steps.push("Boil water");
 steps.push("Add " + teaType);
 steps.push("Steep for 5 minutes");
 steps.push("Serve hot");
 return steps.join("\n");
}

console.log(prepareTea("green tea"));
// Output: Boil water
// Add green tea
// Steep for 5 minutes
// Serve hot

function calculateCircleArea(radius) {
 if (radius <= 0) {
 return "Error: Radius must be greater than zero.";
 }

 const pi = Math.PI; // Use Math.PI for more accuracy
 const squaredRadius = radius * radius;
 const area = pi * squaredRadius;
 const roundedArea = Math.round(area * 100) / 100; // Round to 2 decimal
places
 return "Calculated area: " + roundedArea;
}

console.log(calculateCircleArea(5));
// Output: Calculated area: 78.54

console.log(calculateCircleArea(-1));
// Output: Error: Radius must be greater than zero.
Enter fullscreen mode Exit fullscreen mode
  • I/O operations: I/O is short for input and output. Functions may handle interactions with system components like the screen, storage, logs, or network.
async function fetchData(url) {
 const response = await fetch(url);
 const data = await response.json();
 return data;
}

fetchData('https://jsonplaceholder.typicode.com/todos/1')
 .then(data => console.log(data));
// {
// "userId": 1,
// "id": 1,
// "title": "delectus aut autem",
// "completed": false
// }
Enter fullscreen mode Exit fullscreen mode

Methods

Methods are functions attached to objects. They allow you to perform operations on the object's data. In JavaScript, methods are commonly used with objects, arrays, and other built-in types.

const person = {
 name: 'John',
 greet: function() {
 console.log('Hello, ' + this.name);
 }
};

// Calling the greet method.
person.greet(); // Output: Hello, John
Enter fullscreen mode Exit fullscreen mode

Earlier in this article, you saw how to use methods of the Map and Set objects.

Primitives also have methods because under the hood everything in JavaScript is an object.

const greeting = 'Hello, world';

// Changing case.
console.log(greeting.toUpperCase()); // Output: HELLO, WORLD

// Replacing part of the string.
console.log(greeting.replace('Hello', 'Goodbye')); // Output: Goodbye,
world

// Checking if it includes a substring.
console.log(greeting.includes('world')); // Output: true
Enter fullscreen mode Exit fullscreen mode

Noop

A "noop" stands for "no operation." It describes a function, operation, or command that does nothing. This can be useful for placeholders in code or to intentionally have no effect.

In JavaScript, a noop function looks like this:

function noop() {}
const noop = () => {};
Enter fullscreen mode Exit fullscreen mode

Pure functions

A function is a pure function, if:

  1. given the same inputs, it always returns the same output, and
  2. it has no side-effects.

You've seen pure functions already. For example, both sum functions and the square function are pure.

Pure functions are deterministic. This is captured by the first property. A pure function will always produce the same output for the same set of inputs, no matter when or how many times it is called. This predictability is a key property of pure functions and is essential for reliable and testable code.

Here is an example for a function that violates the first rule.

/**
 * Generates a random integer between the start and end values, both
inclusive.
 *
 * @param {number} start - The starting value.
 * @param {number} end - The ending value.
 * @returns {number} - A random integer between the start and end.
 */
export const generateRandomInt = (start, end) =>
 Math.round(Math.random() * (end - start) + start);
Enter fullscreen mode Exit fullscreen mode

generateRandomInt can be called with the same start and end values, but it produces different results because it uses Math.random().

And here is a example, which violates the second rule.

let externalArray = [];
function sideEffectingFunction(x) {
 externalArray.push(x); // Modifies external array
 return x;
}

console.log(sideEffectingFunction(5)); // 5, modifies externalArray
console.log(sideEffectingFunction(10)); // 10, modifies externalArray, too
console.log(externalArray); // Output: [5, 10]
Enter fullscreen mode Exit fullscreen mode

Even though sideEffectingFunction returns something, it pushes code as a side-effect to an array and you can meaningfully call it without using it's return value.

“A dead giveaway that a function is impure is if it makes sense to call it without using its return value. For pure functions, that's a noop.” - Eric Elliott

Idempotence

Another concept you need to know is idempotence.

Idempotence is a property of certain operations in which no matter how many times you perform the operation, the result remains the same after the first application. For example, setting a value to 5 is idempotent because no matter how often you do it, the value remains 5.

let number = 5;
number = 5; // still 5
number = 5; // no change, still 5
Enter fullscreen mode Exit fullscreen mode

All pure functions are idempotent, but not all idempotent functions are pure functions.
An idempotent function can cause idempotent side-effects.
A pure function cannot.
Deleting a record in a database by ID is idempotent, because the row of the table stays deleted after subsequent calls. Additional calls do nothing.

Here is a synchronous example.

const uniqueItems = new Set();
// This custom addItem function is idempotent because ...
function addItem(item) {
 uniqueItems.add(item);
 console.log(`Added item: ${item}`);
 return uniqueItems.size;
}

addItem("apple"); // Outputs "Added item: apple", returns 1
// ... calling addItem with the same item twice leaves the set unchanged.
addItem("apple"); // Outputs "Added item: apple", returns 1
addItem("banana"); // Outputs "Added item: banana", returns 2
Enter fullscreen mode Exit fullscreen mode

Referential Transparency

Idempotent functions without side-effects have a feature known as referential transparency.

That means that if you have a function call:

const result = square(7);
Enter fullscreen mode Exit fullscreen mode

You could replace that function call with the result of square(7) without changing the meaning of the program. So, for example if the result of square(7) is 49. Therefore, you could change the code above to:

const result = 49;
Enter fullscreen mode Exit fullscreen mode

and your program would still work the same.

Functional Programming Prerequisites

A language needs three features to support functional programming, and JavaScript has all three:

  • First-class functions (and therefore higher-order functions),
  • closures, and
  • anonymous functions and concise lambda syntax.

First class functions

In JavaScript, functions are treated as first-class citizens. This means that functions can be stored in variables. Therefore, you can use functions as:

  • arguments to other functions,
  • return values from functions,
  • values to an objectʼs keys
// Storing a function in a variable.
const greet = function() {
 return "Hello, World!";
}

// Passing a function as an argument (callback).
function shout(fn) {
 const message = fn();
 console.log(message.toUpperCase());
}

shout(greet); // Output: "HELLO, WORLD!"

// Returning a function from another function.
function multiply(multiplicand) {
 return function (multiplier) {
 return multiplicand * multiplier;
 }
}

const double = multiply(2);
console.log(double(5)); // Output: 10
Enter fullscreen mode Exit fullscreen mode

If you want to learn what first-class functions enable in React check out this article about higher-order components, link is in the description below.

Higher-order functions

The last example multiply that you saw is called a "higher-order function" because when you call it for the first time with a number, it returns a function.

Any function that takes in or returns a function is called a higher order function.

function greet() {
 return "Hello World!"
}

const identity = x => x; // Same as myFunction from earlier ❗
identity(greet)(); // "Hello World!"
Enter fullscreen mode Exit fullscreen mode

Closure

The multiply function from earlier hid another key concept: Closure.

A closure happens when a function is bundled together with it's lexical scope. In other words, a closure gives you access to an outer functionʼs scope from an inner function. Closures in JavaScript are created whenever a function is created, at function creation time.

To use a closure, define a function inside another function and expose it by returning the inner function or passing it to another function.
The inner function can access variables in the outer function's scope, even after the outer function has returned.

function multiply(multiplicand) {
 return function(multiplier) {
 return multiplicand * multiplier;
 }
}

// Multiplicand of 2 gets captured in the closure because the inner
// returned function has access to it, even though the outer `multiply`
// function already ran to completion.
const double = multiply(2);
console.log(double(5)); // Output: 10
Enter fullscreen mode Exit fullscreen mode

Closures serve three purposes:

  1. They provide data privacy for objects.
  2. In functional programming, they enable partial application and currying.
  3. They act as parameters for callback or higher-order functions like map, reduce, and filter.

You're going to see 2.) and 3.) later in this article, so let's take a look at the concept of data privacy.

const createCounter = () => {
 let count = 0;
 return function increment() {
 count = count + 1;
 return count;
 }
}

const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // 1
counter1(); // 2
counter2(); // 1
let capturedCount = counter1(); // 3
capturedCount = capturedCount + 39; // 42
counter1(); // 4
Enter fullscreen mode Exit fullscreen mode

No external influence can manipulate the count value of counter1. You need to call counter1 to increment it.

This matters because some applications require private state. A common pattern is to prefix the private key with __.

const user = {
 __secretKey: 'abcj'
}
Enter fullscreen mode Exit fullscreen mode

However, junior developers might not know that __ signals: "Do NOT change this key.", so they mutate it. And senior developers sometimes think it's okay to change it because they believe they know better.

Closures give you a reliable way to enforce data privacy for everyone.

Imperative vs Declarative

As you learned at the start of this article, functional programming is declarative. But what does that mean?

Imperative code describes "how" to do things. The code contains the specific steps needed to achieve your desired result.

Declarative code describes "what" to do. The "how" gets abstracted away.
In other words, imperative programming is about defining the process to reach an outcome. This is known as flow control, where you dictate each step of the computation.

Declarative programming, on the other hand, is about defining the outcome, known as data flow. Here, you describe what you want, and the system determines how to achieve it.

Here is an example for imperative code.

let numbers = [1, 2, 3, 4, 5];
let doubled = [];
for (let i = 0; i < numbers.length; i++) {
 doubled.push(numbers[i] * 2);
}
console.log(doubled); // Output: [2, 4, 6, 8, 10]
Enter fullscreen mode Exit fullscreen mode

And here is another example of imperative code, but this time with a custom function.

function filterEvens(numbers) {
 let index = 0;

 while (index < numbers.length) {
 if (numbers[index] % 2 !== 0) {
 // Removes the item at the current index if it's odd.
 numbers.splice(index, 1);
 } else {
 // Only move to the next index if the current item was not removed
 // because the current index gets taken by the value after the
 // deleted one.
 index++;
 }
 }
}

let numbers = [1, 2, 3, 4, 5];
filterEvens(numbers);
console.log(numbers); // Output: [2, 4]
Enter fullscreen mode Exit fullscreen mode

Before you look at declarative code, you need to understand immutability and abstraction.

Immutability

Immutability in programming means that an object or value cannot be modified after it is created; instead, any changes result in a new object or value.

const originalArray = [1, 2, 3];
// Creates a new array by spreading the original and adding a new element.
const newArray = [...originalArray, 4];

console.log(originalArray); // Output: [1, 2, 3]
console.log(newArray); // Output: [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Similarly, mutable state is state that can be modified after you created it.

const array = [1, 2, 3];
array.push(4); // Modifies the original array by adding a new element.

console.log(array); // Output: [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Immutability is a central concept of functional programming because with it, the data flow in your program is preserved. State history is maintained, and it helps prevent strange bugs from creeping into your software.

“non-determinism = parallel processing + mutable state” - Martin Odersky

You want determinism to make your programs easy to reason about, and parallel processing to keep your apps performant, so you have to get rid of mutable state.

Generally speaking, when you write your code using functional programming it becomes more
deterministic, easier to reason about, easier to maintain, more modular and more testable.

Abstraction

Abstraction is a fundamental concept in programming that involves hiding the complex reality while exposing only the necessary parts of an object or a system.

There are two types of abstraction: generalization and specialization.

Generalization is when you create a more universal form of something for use in multiple contexts. This process identifies common features among different components and develops a single model to represent all variations.

This is what most people think of when they hear "abstraction" and what Eric Elliott refers to when he says:

Junior developers think they have to write a lot of code to produce a lot of value.
Senior developers understand the value of the code that nobody needed to write.” - Eric Elliott

Specialization is when you apply the abstraction to a specific use-case and add what makes the current situation different.

The hard part is knowing when to generalize and when to specialize. Unfortunately, there is no good rule of thumb for this - you develop a feel for both with experience.

And what you're going to find is:
Abstraction is the key to simplicity.

“Simplicity is about subtracting the obvious and adding the meaningful.” - John Maeda

Using the functional programming paradigm you can create the most beautiful abstractions.

At this point you're probably starving for examples and you're going to see some soon.

Now, it's time to look at declarative code, which will also show you more immutability and some abstraction.

Array methods

Remember, declarative programming is about "what" to do.

The perfect example of declarative code in JavaScript are the native array methods. You're going to see the three most common ones: map, filter and reduce.

map

Take a look at the map function first. It does exactly what we did earlier with the for loop, but the code is a lot more concise.

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // Output: [2, 4, 6, 8, 10]
Enter fullscreen mode Exit fullscreen mode

The map method is a perfect example of abstraction.

map removes the obvious: iterating over the array and changing each value. Since it takes in a function, the map method is a higher-order function.

You only supply the meaningful: the function that doubles a number, which map applies to every number in the array. This double function is an anonymous function using the concise lamda syntax.

In general, a concise lambda is a simplified way to write a function with minimal syntax. In JavaScript it refers to the arrow function syntax.

map is also immutable because it returns a new array. numbers and doubled are two distinct arrays and the numbers array still contains the numbers 1 through 5. You can verify this by mapping using the identity function that returns its input.

const numbers = [1, 2, 3, 4, 5];
const clone = numbers.map(x => x);
console.log(numbers === clone); // false
Enter fullscreen mode Exit fullscreen mode

Even though numbers and clone are both an array with the numbers 1 through 5, they are different array instances.

You might be asking yourself: "Isn't that generating tons of data that no one uses?"
Well kinda, but compared to what modern laptops are capable of the data amount is tiny, and JavaScript has garbage collection which clears up the stale memory.

filter

const fruits = ['apple', 'banana', 'citrus'];
const containsA = fruits.filter(fruit => fruit.includes('a'));
// Output: ['apple', 'banana'];
Enter fullscreen mode Exit fullscreen mode

The filter method takes in a special function called the "predicate". A predicate is a function that always returns only a boolean. It tests each element in the array. If it returns true, the element is included in the resulting array.

filter also returns a new array aka. it's immutable.

reduce

The reduce method in JavaScript processes an array to produce a single output value. It takes a reducer function and an optional initial value. The reducer function itself accepts two parameters: an accumulator (which holds the accumulated result) and the currentValue from the array. The reduce method is also immutable.

Here is an example where you can use reduce on an array of numbers from 1 to 4, summing them together.

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

const sumReducer = (accumulator, currentValue) => accumulator +
currentValue;

const total = numbers.reduce(sumReducer, 0); console.log(total); //
Output: 10
Enter fullscreen mode Exit fullscreen mode

In this example, reduce is called on the numbers array. The sumReducer function is used to add each number to a running total, starting from 0. Here's what happens at each step:

Step Accumulator Current Value Operation New Accumulator Value
1 0 1 0+1 1
2 1 2 1+2 3
3 3 3 3+3 6
4 6 4 6+4 10

Details of the Process:

  • Step 1: The accumulator starts at 0 (the initial value). The current value is the first element of the array, which is 1. They are added together to make the new accumulator value 1.
  • Step 2: The accumulator is now 1. The current value is the next element in the array, 2. Adding these gives 3.
  • Step 3: The accumulator is now 3, and the current value is 3. Adding these gives 6.
  • Step 4: The accumulator is now 6, and the current value is the last element, 4. Adding these gives the final result of 10.

At the end of the process, the reduce method returns 10, which is the sum of all elements in the array. This demonstrates how reduce can be used to transform an array into a single value through repeated application of a function.

reduce is the most powerful method because you can implement map and filter with reduce, but neither filter nor reduce with map and neither map or reduce with filter.

You can implement map with reduce like this:

const mapUsingReduce = (array, mapFunction) =>
 array.reduce(
 (accumulator, current) => [...accumulator, mapFunction(current)],
 [],
 );

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

const doubled = mapUsingReduce(numbers, x => x * 2);
console.log(doubled); // Output: [2, 4, 6, 8]
Enter fullscreen mode Exit fullscreen mode

You can implement filter with reduce like this:

const filterUsingReduce = (array, filterFunction) =>
 array.reduce(
 (accumulator, current) =>
 filterFunction(current) ? [...accumulator, current] : accumulator,
 [],
 );

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

const evens = filterUsingReduce(numbers, x => x % 2 === 0);
console.log(evens); // Output [2, 4]
Enter fullscreen mode Exit fullscreen mode

Expressions over Statements

In functional programming, you'll see many expressions and few statements. Expressions avoid
intermediate variables, while statements often bring side-effects and mutable state.

Statements

Imperative code frequently utilizes statements. A statement is a piece of code that performs an action.

  • Loops - for, while, etc.
// Loops:
// A for loop that logs numbers 0 to 4.
for (let i = 0; i < 5; i++) {
 console.log(i);
}
// A while loop that decrements x and logs it until x is no longer greater
than 0.
while (x > 0) {
 x--;
 console.log(x);
}
Enter fullscreen mode Exit fullscreen mode
  • Control flow - if, switch, etc.
// An if...else statement that logs if x is positive or not.
if (x > 0) {
 console.log("x is positive");
} else {
 console.log("x is zero or negative"); 
}
// A switch statement that handles different color cases.
switch (color) {
 case "red":
 console.log("Color is red");
 break;
 case "blue":
 console.log("Color is blue");
 break;
 default:
 console.log("Color is not red or blue");
}
Enter fullscreen mode Exit fullscreen mode
  • Error handling - try...catch, throw, etc.
// A try...catch block that handles errors from riskyFunction.
try {
 let result = riskyFunction();
} catch (error) {
 console.error(error);
}
throw new Error("Something went wrong"); // Throws a new error with a
message.
Enter fullscreen mode Exit fullscreen mode

Except for functions, if it's a keyword with curly braces, you're likely dealing with a statement. (❗)

Expressions

Declarative code favors expressions. An expression evaluates to a value.

  • Literal expressions
42; // The number 42 is a literal expression.
"Hello"; // The string "Hello" is a literal expression.
Enter fullscreen mode Exit fullscreen mode
  • Arithmetic expressions
5 + 3; // Evaluates to 8.
x * y; // Evaluates to the product of x and y.
Enter fullscreen mode Exit fullscreen mode
  • Logical expressions
true && false; // Evaluates to false.
x || y; // Evaluates to x if x is true, otherwise y.
Enter fullscreen mode Exit fullscreen mode
  • Function expressions
const funcExpr = function() { return 42; }; // Defines a function
expression.
const arrowFunc = () => 42; // Defines an arrow function expression.
Enter fullscreen mode Exit fullscreen mode
  • Object and array initializers
{ name: "John", age: 30 }; // Object initializer expression.
[1, 2, 3]; // Array initializer expression.
Enter fullscreen mode Exit fullscreen mode
  • Property access expressions
obj.name; // Accesses the "name" property of obj.
array[0]; // Accesses the first element of array.
Enter fullscreen mode Exit fullscreen mode
  • Function calls
square(7); // Evaluates to 49.
Math.max(4, 3, 2); // Evaluates to 4.
Enter fullscreen mode Exit fullscreen mode

Function composition

You’re about to unlock a new understanding of code and gain superpowers, so "lock-in".

“All software development is composition: The act of breaking a complex problem down to smaller parts, and then composing those smaller solutions together to form your application.” - Eric Elliot

Whenever you use functions together, you're "composing" them.

const increment = n => n + 1;
const double = n => n * 2;
function doubleInc(n) {
 const doubled = double(n);
 const incremented = increment(doubled);
 return incremented;
}

doubleInc(5); // 11
Enter fullscreen mode Exit fullscreen mode

But with anything in life, you can do it better if you do it consciously. The code above is actually NOT the ideal way to write it because:

The more code you write, the higher the surface area for bugs to hide in.

less code = less surface area for bugs = less bugs

The obvious exception is clear naming and documentation. It's fine if you give a function a longer name and supply doc strings to make it easier for your readers to understand your code.

You can reduce the surface area for bugs by avoiding the capturing of the intermediary results in variables.

const increment = n => n + 1;
const double = n => n * 2;

const doubleInc = n => inc(double(n));
doubleInc(5); // 11
Enter fullscreen mode Exit fullscreen mode

In mathematics, function composition is taking two functions f and g and applying one function to the result of another function: h(x) = (f ∘ g)(x) = f(g(x)). Note: The hollow dot is called the composition operator.

In mathematical notation, if you have two functions f and g, and their composition is written as (f ∘ g)(x), this means you first apply g to x and then apply f to the result of g(x). For your example, f(n) = n1 and g(n) = 2n, the composition h(n) = f(g(n)) calculates 2n + 1.

Note: Generally mathematicians use x (or y or z etc.) to represent any variable, but in the code example above n is used to subtly hint at the parameter to be a number. The different name has no impact on the result.

You can abstract away the composition operator into a function called compose2 which takes two functions and composes them in mathematical order.

const compose2 = (f, g) => x => f(g(x)); // ∘ operator

const increment = n => n + 1; // f(n)
const double = n => n * 2; // g(n)

const doubleInc = compose2(increment, double); // h(n)
doubleInc(5); // 11
Enter fullscreen mode Exit fullscreen mode

compose2 only works for two functions at a time.

But get ready, because this is where it gets powerful.

If you want to compose an arbitrary amount of functions, you can write a generalized compose.

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
// reduceRight works like reduce, but iterates the array from
// the last item to the first item.

const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;

const incDoubleSquare = compose(square, double, increment);
incDoubleSquare(3); // 64
Enter fullscreen mode Exit fullscreen mode

The compose function here is written using the mathematical variable names. If you want to take the names that you might be used to from reduce, then you'd write it like this:

const compose =
 (...functions) =>
 initialValue =>
 functions.reduceRight(
 (accumulator, currentFunction) => currentFunction(accumulator),
 initialValue,
 );

const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;

const incDoubleSquare = compose(square, double, increment);
incDoubleSquare(3); // 64
Enter fullscreen mode Exit fullscreen mode

compose takes multiple functions as its arguments, and collects them into an array fns via the rest syntax.

It then returns a child function that takes in the initialValue x and returns the array fns with the
reduceRight method applied to it. The reduceRight method then takes in a callback function and the initialValue x.

That callback function is the heart of compose. It takes in the accumulator y (starting from x) and the currentValue f, which is a function from the array fns. It then returns that function f - called with the accumulator y.

The result of that function call, then becomes the accumulator y for the next iteration. Here the next function f from fns get called with that new accumulator value. This repeats until the initialValue x has been passed and transformed through all functions from the array fns.

Function Call Accumulator y currentValue f Operation New Accumulator
Initial value 3 - - 3
increment 3 increment increment(3) = 3 + 1 4
double 4 double double(4) = 4 * 2 8
square 8 square square(8) = 8 * 8 64

Many people who are used to reading texts from left to right find it unintuitive to compose functions in mathematical order. Many functional programming packages provide another function called commonly pipe, which composes function from left to right in reverse mathematical order.

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;

const incDoubleSquare = pipe(increment, double, square);
incDoubleSquare(3); // 64
Enter fullscreen mode Exit fullscreen mode

trace

You might be asking right now. "But way, what if you want to debug your code? Then you need to capture the intermediate results in variables, right?"

You actually do not. You only need a helper higher-order function called trace.

const trace = msg => x => {
 console.log(msg, x);
 return x;
}
Enter fullscreen mode Exit fullscreen mode

And here is how you can use it.

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = msg => x => {
 console.log(msg, x);
 return x;
}

const increment = n => n + 1;
const double = n => n * 2;
const square = n => n * n;

const incDoubleSquare = pipe(
 increment,
 trace('before double'),
 double,
 trace('after double'),
 square
);
incDoubleSquare(3); // 64
// Also logs out:
// before double 4
// after double 8
Enter fullscreen mode Exit fullscreen mode

Currying

You saw another technique being used called currying.

Currying is a transformation of functions that translates a function from callable as f(a, b, c) into callable as f(a)(b)(c). In other words, a function is curried if it can take in each of it's parameters one at a time.

function addUncurried(a, b) {
 return a + b;
}

function addCurried(a) {
 return function(b) {
 return a + b;
 }
}

addUncurried(41, 1); // 42
addCurried(41)(1); // 42
Enter fullscreen mode Exit fullscreen mode

With arrow functions, these definitions can become one-liners by leveraging their implicit returns.

const addUncurried = (a, b) => a + b;

const addCurried = a => b => a + b;

addUncurried(41, 1); // 42
addCurried(41)(1); // 42
Enter fullscreen mode Exit fullscreen mode

Here a addCurried is a function that takes in a number a and returns a function b => a + b. You can read it like this:

const addCurried = (a => (b => a + b));
Enter fullscreen mode Exit fullscreen mode

You can curry any function. For example, you can create custom map and reduce functions.

const map = fn => arr => arr.map(fn);
const reduce = fn => x => arr => arr.reduce(fn, x);
Enter fullscreen mode Exit fullscreen mode

Map takes in two parameters and reduce takes in three. The number of parameters a function expects in its definition is called arity.

There are shorthand terms for functions that take in 1, 2 and 3 parameters.

  • Unary function: Takes one argument, e.g. square.
  • Binary function: Takes two arguments, e.g. map.
  • Ternary function: Takes three arguments, e.g. reduce.

In the context of currying, understanding arity is important because each step in a curried function reduces the arity by one until all expected arguments have been received and the final operation can be performed.

Additionally, the first function of a composition can have any arity, but ever following function needs to be unary.

const add3 = (a, b, c) => a + b + ; // Ternary
const double = n => n * 2; // Unary

const addThenDouble = pipe(add3, double);
addThenDouble(6, 7, 8); // 42
Enter fullscreen mode Exit fullscreen mode

Exercise: create your own custom filter function that is curried and takes in a predicate pred and then an array arr and then filters the array based on the predicate.

Wouldn't it be nice if you had a function that can curry any function?

const addUncurried = (a, b) => a + b;

const curry = /* ... magic? ... */

const addCurried = curry(addUncurried);
Enter fullscreen mode Exit fullscreen mode

Well there it is.

const addUncurried = (a, b) => a + b;

const curry = (f, array = []) =>
 (...args) =>
 (a => (a.length >= f.length ? f(...a) : curry(f, a)))([
 ...array,
 ...args,
 ]);

// NOTE: because of f.length, this implementation of `curry` fails with
// functions that use default parameters.
const addCurried = curry(addUncurried);

const increment = addCurried(1);
increment(4); // 5
addCurried(1, 4); // 5
addCurried(1)(4); // 5
Enter fullscreen mode Exit fullscreen mode

The previous example used the mathematical names for the variables .If you want to name the variables more descriptively to understand curry now, you can write the function like this:

const curry =
 (targetFunction, collectedArguments = []) =>
 (...currentArguments) =>
 (allArguments =>
 allArguments.length >= targetFunction.length
 ? targetFunction(...allArguments)
 : curry(targetFunction, allArguments))([
 ...collectedArguments,
 ...currentArguments,
 ]);
Enter fullscreen mode Exit fullscreen mode

curry uses recursion, which is when a function calls itself for the purpose of iteration. Let's break it down:

  • f or targetFunction: The original function you want to curry.
  • array or collectedArguments: An array to collect the arguments of all currying calls. Each cycle (except for the last) new arguments get added.
  • args or currentArguments: An array of the arguments taken in with the current invocation of the curried function (in this case: addCurried).
  • a or allArguments: An array of arguments concatenated from the currentArguments and the collectedArguments from the previous calls.

When we call addCurried with one or more arguments, these current arguments get taken in as args in the child function of curry. This child function returns another function (grand-child function of curry).

The grand-child function immediately invokes itself with an array concatenated from the collected arguments array and the current arguments args, receiving these values from its grand-parent and parent closure. It takes this new array as the all arguments a and checks if the all arguments a contains the same or higher amount of arguments as the target function f is declared with.

If yes, this means all necessary currying calls have been performed: It returns a final call of the target function f invoked with the all arguments a. If not, it recursively invokes curry once again with the target function f and the new all arguments a, which then get taken in as the new collected arguments array.
This repeats until the a.length condition is met.

Side notes:

  • During the first call, the all arguments a and the current arguments args are the same value, as the collected arguments array has nothing added to it yet.
  • The grand-child function's immediate invocation happens before its parent function completes.

Partial Application

As you learned earlier, an application happens when the arguments are used to replace the function's parameters. This allows the function to perform its task using the provided arguments.

const add = (a, b) => a + b;
const inc = n => add(1, n);
Enter fullscreen mode Exit fullscreen mode

A partial application is the process of applying a function to some, but not all, of its arguments. This creates a new function, which you can store in a variable for later use. The new function needs fewer arguments to execute because it only takes the remaining parameters as arguments.

Partial applications are useful for specialization when you want to reuse a function with common
parameters.

const add = a => b => a + b;
const inc = add(1); // point-free
const incPointed = n => add(1)(n); // pointed
Enter fullscreen mode Exit fullscreen mode

Point-Free Style
inc is defined in point-free style, which is when you write a function without mentioning the parameters.

inc uses closure because the argument 1 is captured in the closure of add as a.
There are two requirements for your code to be composable: "data last" and the data needs to line up.

Data First vs. Data Last

"data last" means the data your functions operate on should be their last parameter.
For the add and multiply functions you saw earlier the order of arguments is irrelevant because they are commutative.

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

const a = 41;
const b = 1;
console.log(add(41, 1) === otherAdd(41, 1)); // true
Enter fullscreen mode Exit fullscreen mode

But division is NOT, so you're going to learn the importance of the "data last" principle using a divide function.

const divideDataLast = (y) => (x) => x / y;
const divideDataFirst = (x) => (y) => x / y;
Enter fullscreen mode Exit fullscreen mode

Let's say you want to specialize and create a halve function that takes in a number and divides it by two.

Using the "data first" function, it's impossible for you to define a halve function in point-free style.

const divideDataLast = (y) => (x) => x / y;

const divideDataFirst = (x) => (y) => x / y;

const halve = divideDataLast(2);
// const halveFail = divideDataFirst(2); 🚫 fails
const halvePointed = (n) => divideDataFirst(n)(2);
Enter fullscreen mode Exit fullscreen mode

halfFail captures 2 in the closure of divideDataFirst. It is a function that takes in a number and when called divides 2 by that supplied number.

In general, you need to write your functions using the "data last" principle to enable partial application.

BTW, the best library for functional programming in the "data last" paradigm is Ramda. Here is an outlook what you can do with Ramda. You can understand this in-depth some time in the future.

import { assoc, curry, keys, length, pipe, reduce, values } from 'ramda';

const size = pipe(values, length);

size({ name: 'Bob', age: 42 }); // 2
size(['a', 'b', 'c', 'd']); // 4

const renameKeys = curry((keyReplacements, object) =>
 reduce(
 (accumulator, key) =>
 assoc(keyReplacements[key] || key, object[key], accumulator),
 {},
 keys(object),
 ),
);

const input = { firstName: 'Elisia', age: 22, type: 'human' };
const keyReplacements = { firstName: 'name', type: 'kind', foo: 'bar' };

renameKeys(keyReplacements)(input);
// Output: { name: 'Elisia', age: 22, kind: 'human' }
Enter fullscreen mode Exit fullscreen mode

size is a function that can take in objects or arrays and returns how many properties or elements it has.

Remember, functional programming is meant to improve your code by making it more bug-free, modular, testable, refactorable, understandable and deterministic. So, if you mix imperative and declarative code, like in the definition for renameKeys that is totally fine. Relax if you need some time to understand implementations like this. As you play around with this new paradigm, go with what's easiest for you to write. There is no need to force functional programming.

You can call renameKeys with keyReplacements and an input to return a new object with it's keys renamed according to the keyReplacements.

Data Needs to Line Up

Similarly, only with "data last" can you compose functions effectively because the types of the arguments and return values of functions have to line up to compose them. For example, you can't compose a function that accepts an object and returns a string with a function that receives an array and returns a number.

// (number, number) => number[]
const echo = (value, times) => Array(times).fill(value);
// number[] => number[]
const doubleMap = array => array.map(x => x * 2);

// Correct composition. ✅
const echoAndDoubleMap = compose(doubleMap, echo);

// Reminder, the first function in a composition does NOT need to
// be unary, and echo is binary.
// echoAndDoubleMap starts binary and ends unary.
console.log(echoAndDoubleMap(3, 4)); // [6, 6, 6, 6]

// Incorrect composition that will throw an error. ❌
const wrongOrder = compose(echo, doubleMap);

try {
 // This will fail because doubleMap expects an array,
 // instead of two numbers.
 console.log(wrongOrder(3, 4));
} catch (error) {
 console.error("Error:", error.message); // Error: array.map is not a
function
}
Enter fullscreen mode Exit fullscreen mode

Mozart

Now it's your turn. Try the following exercise to practice what you've learned from this article. If you get stuck, you can always scroll up and read it again. If you prefer not to do the exercise, just read the solution and follow along.

Create a function that takes in an array of numbers and filters all the even numbers (so it rejects the odd numbers), then doubles all the even numbers and lastly, sums up the result. Break down your functions to their most basic abstractions then compose them point-free. (Hint: You'll need to look up the modulo operator % to check if a number is even.)

const curry =
 (f, array = []) =>
 (...args) =>
 (a => (a.length >= f.length ? f(...a) : curry(f, a)))([
 ...array,
 ...args,
 ]);

const add = curry((a, b) => a + b);
const multiply = a => b => a * b;

const inc = add(1);
const double = multiply(2);
const isEven = n => n % 2 === 0;

const map = fn => arr => arr.map(fn);
const filter = pred => arr => arr.filter(pred);
const reduce = curry((fn, acc, arr) => arr.reduce(fn, acc));

const doubleMap = map(double);
const filterEvens = filter(isEven);
const sum = reduce(add, 0);

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const mozart = pipe(filterEvens, doubleMap, sum);

mozart([1, 2, 3, 4, 5]); // 12
Enter fullscreen mode Exit fullscreen mode

Lastly, is this more complicated than simply writing the following?

const mozart = numbers =>
 numbers
 .filter(n => n % 2 === 0)
 .map(n => n * 2)
 .reduce((a, b) => a + b, 0);
mozart([1, 2, 3, 4, 5]); // 12
Enter fullscreen mode Exit fullscreen mode

Yes, absolutely! You should always use the simplest implementation for your requirements, also known as KISS (Keep It Simple, Stupid), or YAGNI (You Ain't Going To Need It).

Functional programming shines as your app grows and you need to generalize and specialize your code so it scales well and stays maintainable. This way your code is more modular, and way easier to test, to reuse and to refactor. Future articles will show you real-world examples how to use these techniques.

You now know the 20% of functional programming that gives you 80% of the result.

Top comments (0)