Skip to main content

JavaScript Function Composition

Function composition is a powerful concept in functional programming that allows you to combine multiple functions into a single function. This technique enables you to build complex operations by chaining together smaller, simpler functions. In this tutorial, we'll explore function composition in JavaScript and how it can make your code more modular, reusable, and easier to reason about.

What is Function Composition?

Function composition takes the output from one function and passes it as an input to another function. The mathematical notation for composing two functions f and g is typically written as (f ∘ g)(x) or simply f(g(x)).

In JavaScript, instead of writing nested function calls like f(g(x)), function composition allows us to create a new function that represents this combination.

Basic Function Composition

Let's start with a simple example to understand the concept:

javascript
// Two simple functions
const double = x => x * 2;
const increment = x => x + 1;

// Without composition (nested function calls)
const result1 = double(increment(5));
console.log(result1); // Output: 12 (5 + 1 = 6, then 6 * 2 = 12)

// Manual composition
const doubleAfterIncrement = x => double(increment(x));
const result2 = doubleAfterIncrement(5);
console.log(result2); // Output: 12

In this example, we have two simple functions: double and increment. The doubleAfterIncrement function is a composition of these two functions.

Creating a Compose Function

Rather than manually composing functions each time, we can create a utility function called compose that automates the process:

javascript
// A simple compose function that works with two functions
const compose = (f, g) => x => f(g(x));

const doubleAfterIncrement = compose(double, increment);
console.log(doubleAfterIncrement(5)); // Output: 12

The compose function takes two functions as arguments and returns a new function. When called, this new function passes its input to the second function (g), then passes that output to the first function (f).

Composition with Multiple Functions

We can extend our compose function to work with any number of functions:

javascript
// Advanced compose that works with any number of functions
const compose = (...functions) => {
return functions.reduce((acc, fn) => {
return (...args) => acc(fn(...args));
});
};

// Or a more readable implementation
const compose = (...functions) => x =>
functions.reduceRight((value, fn) => fn(value), x);

// Example functions
const double = x => x * 2;
const increment = x => x + 1;
const square = x => x * x;

// Compose multiple functions (executed from right to left)
const doubleSquareAfterIncrement = compose(double, square, increment);

console.log(doubleSquareAfterIncrement(5)); // Output: 72
// Execution: increment(5) = 6, then square(6) = 36, then double(36) = 72

Notice that in both implementations, the functions are executed from right to left (mathematical convention). The reduce implementation might be harder to understand initially but is very powerful.

The Pipe Function (Left-to-Right Composition)

Sometimes, it's more intuitive to read function composition from left to right. We can create a pipe function for this:

javascript
const pipe = (...functions) => x => 
functions.reduce((value, fn) => fn(value), x);

// Using pipe (executed from left to right)
const incrementSquareDouble = pipe(increment, square, double);

console.log(incrementSquareDouble(5)); // Output: 72
// Execution: increment(5) = 6, then square(6) = 36, then double(36) = 72

The pipe function is similar to compose, but it executes functions from left to right, which can be more natural to read for some developers.

Real-World Example: Data Transformation Pipeline

Let's look at a practical example of how function composition can help with data transformation:

javascript
// A set of reusable transformation functions
const removeEmptyValues = arr => arr.filter(item => item !== null && item !== undefined && item !== '');
const formatNames = arr => arr.map(name => name.trim().toLowerCase());
const makeUnique = arr => [...new Set(arr)];
const sortAlphabetically = arr => [...arr].sort();

// Sample data
const rawNames = ['John', 'jane ', '', null, 'JOHN', 'Bob ', undefined, 'alice'];

// Create a processing pipeline using compose
const processNames = pipe(
removeEmptyValues,
formatNames,
makeUnique,
sortAlphabetically
);

const processedNames = processNames(rawNames);
console.log(processedNames);
// Output: ["alice", "bob", "jane", "john"]

In this example, we have a series of small, focused functions that each perform a single transformation. By composing them together with our pipe function, we create a clean data transformation pipeline that's easy to understand and maintain.

Debugging Composed Functions

When working with composed functions, debugging can be challenging. One approach is to create a debug function that logs values as they pass through the pipeline:

javascript
const debug = label => value => {
console.log(`${label}: `, value);
return value;
};

const processNames = pipe(
removeEmptyValues,
debug('After removing empty'),
formatNames,
debug('After formatting'),
makeUnique,
debug('After making unique'),
sortAlphabetically
);

const processedNames = processNames(rawNames);
// Console output:
// After removing empty: ["John", "jane ", "JOHN", "Bob ", "alice"]
// After formatting: ["john", "jane", "john", "bob", "alice"]
// After making unique: ["john", "jane", "bob", "alice"]
// Final result: ["alice", "bob", "jane", "john"]

The debug function is a higher-order function that logs the value with a label and then returns the value unchanged, allowing it to be inserted anywhere in the composition chain.

Point-Free Style

Function composition often pairs well with "point-free" (or "tacit") programming, where we define functions without explicitly mentioning their arguments:

javascript
// Regular style
const isEven = x => x % 2 === 0;
const numbers = [1, 2, 3, 4, 5, 6];

// Using the function directly with arguments
const evenNumbers = numbers.filter(num => isEven(num));

// Point-free style (no explicit mention of the argument)
const evenNumbers = numbers.filter(isEven);

This style becomes particularly powerful when combined with composition:

javascript
// Some utility functions
const prop = key => obj => obj[key];
const map = fn => array => array.map(fn);
const filter = predicate => array => array.filter(predicate);
const isOver = limit => value => value > limit;

// Point-free composition to get names of products over $100
const getExpensiveProductNames = pipe(
filter(isOver(100)), // Filter products over $100
map(prop('name')) // Map to just the names
);

const products = [
{ name: 'Laptop', price: 1200 },
{ name: 'Phone', price: 800 },
{ name: 'Tablet', price: 200 },
{ name: 'Headphones', price: 50 }
];

console.log(getExpensiveProductNames(products));
// Output: ["Laptop", "Phone", "Tablet"]

Libraries for Composition

While it's good to understand how to implement composition yourself, several libraries provide robust implementations with additional features:

  • Ramda: Offers R.compose and R.pipe with extensive utility functions
  • Lodash/FP: Functional programming variant of Lodash with _.compose and _.flow

Using a library can save time and provide more reliable implementations:

javascript
// Using Ramda
import { compose, filter, map, prop } from 'ramda';

const getExpensiveProductNames = compose(
map(prop('name')),
filter(product => product.price > 100)
);

// Using Lodash/FP
import { compose, filter, map, get } from 'lodash/fp';

const getExpensiveProductNames = compose(
map(get('name')),
filter(product => product.price > 100)
);

Summary

Function composition is a core concept in functional programming that enables you to:

  • Combine multiple simple functions to create complex operations
  • Create reusable and modular code
  • Build clear data transformation pipelines
  • Reduce complexity by focusing on individual operations

By mastering function composition, you can write more elegant, maintainable JavaScript code that follows the functional programming paradigm.

Exercises

To reinforce your understanding, try these exercises:

  1. Implement your own compose and pipe functions from scratch.
  2. Create a data transformation pipeline that:
    • Takes an array of numbers
    • Filters out negative numbers
    • Doubles each remaining number
    • Calculates the sum of the results
  3. Refactor an existing piece of code in your project to use function composition.

Additional Resources

Happy composing!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)