JavaScript Function Composition with Unary Operations

Function composition is a functional programming technique that chains multiple functions together so that the output of one function becomes the input of the next. When dealing with unary operations—functions that accept strictly one argument—JavaScript allows developers to combine these discrete transformations into a single, cohesive execution pipeline. This article explains how unary function composition works mathematically and programmatically, demonstrates standard implementations using pipe and compose, and details why unary constraints are essential for reliable data transformations.

Understanding Unary Functions and Composition

A unary function is any function with an arity of one, meaning it takes a single input and returns a single output:

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

In mathematics, function composition is defined as \((f \circ g)(x) = f(g(x))\). In JavaScript, executing this manually results in nested function calls:

const result = double(increment(4)); // (4 + 1) * 2 = 10

While manual nesting works for two functions, it becomes difficult to read as more functions are added. Automated composition abstracts this pattern into a reusable helper function.

Implementing compose and pipe

Function composition is typically implemented in two directions:

  1. compose (Right-to-Left): Follows traditional mathematical notation where functions are evaluated from the inside out (right to left).
  2. pipe (Left-to-Right): Evaluates functions in reading order (left to right), which aligns more naturally with standard data flow.

Both patterns rely on JavaScript’s Array.prototype.reduce or Array.prototype.reduceRight.

The compose Utility

compose aggregates an array of unary functions using reduceRight:

const compose = (...fns) => initialValue =>
  fns.reduceRight((accumulator, fn) => fn(accumulator), initialValue);

The pipe Utility

pipe processes the array of unary functions in forward order using reduce:

const pipe = (...fns) => initialValue =>
  fns.reduce((accumulator, fn) => fn(accumulator), initialValue);

Complete Example: Transforming Data

Consider a text processing pipeline composed of several small, single-purpose unary operations:

const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const wrapInSpan = str => `<span>${str}</span>`;

// Combine unary operations using pipe
const formatTag = pipe(
  trim,
  toLowerCase,
  wrapInSpan
);

const output = formatTag("  JavaScript Composition  ");
console.log(output); // "<span>javascript composition</span>"

In this pipeline: 1. " JavaScript Composition " is passed to trim, returning "JavaScript Composition". 2. "JavaScript Composition" is passed to toLowerCase, returning "javascript composition". 3. "javascript composition" is passed to wrapInSpan, returning "<span>javascript composition</span>".

Why Unary Operations are Critical for Composition

Composition relies on a contract: each function must return a value compatible with the parameters of the subsequent function. Because a JavaScript function returns only a single value, the next function in the chain can reliably receive only one argument.

If a function requires multiple arguments (n-ary function), it must be converted into a unary function—typically via currying or partial application—before it can participate in a composition pipeline:

// Binary function converted to unary via currying
const multiplyBy = factor => number => number * factor;

const doubleAndIncrement = pipe(
  multiplyBy(2), // returns a unary function: (number) => number * 2
  increment
);

console.log(doubleAndIncrement(5)); // (5 * 2) + 1 = 11

By enforcing unary signatures, function composition ensures modularity, reusability, and predictable data transformations across complex JavaScript applications.