Skip to content
Home
Higher-Order Functions: Map, Filter, and Reduce

Higher-Order Functions: Map, Filter, and Reduce

Functional Programming Functional Programming 8 min read 1669 words Beginner ExcellentWiki Editorial Team

Higher-order functions are functions that operate on other functions — either by taking them as arguments, returning them, or both. They are one of the most practical and widely used concepts from functional programming. Map, filter, and reduce are the quintessential higher-order functions that replace imperative loops with declarative data transformations.

Mastering higher-order functions will make your code more expressive, readable, and less error-prone.

What Is a Higher-Order Function?

A higher-order function is any function that does at least one of the following:

  1. Takes one or more functions as arguments
  2. Returns a function as its result
// takes a function as argument
function applyTwice(f, x) {
    return f(f(x));
---

// returns a function
function multiplyBy(n) {
    return function(x) {
        return x * n;
    };
---

const double = multiplyBy(2);
console.log(double(5)); // 10

Map: Transforming Elements

Map applies a transformation function to each element of a collection and returns a new collection with the transformed values. The original collection is unchanged.

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

Map replaces the imperative pattern of creating an empty array, looping, transforming, and pushing:

// imperative
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
    doubled.push(numbers[i] * 2);
---

// declarative
const doubled = numbers.map(n => n * 2);

The declarative version expresses intent — “double each number” — without detailing the mechanics of iteration.

Map in Different Languages

# Python
doubled = list(map(lambda x: x * 2, numbers))

# Java
List<Integer> doubled = numbers.stream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

# Rust
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();

Filter: Selecting Elements

Filter selects elements from a collection that satisfy a predicate (a function that returns true or false). It returns a new collection containing only the matching elements.

const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter(n => n % 2 === 0);
// evens: [2, 4, 6]

Filter composes naturally with map:

const result = numbers
    .filter(n => n % 2 === 0)
    .map(n => n * 10);
// result: [20, 40, 60]

Reduce: Aggregating Values

Reduce (also called fold) is the most general of the three. It iterates over a collection, accumulating a value at each step. Map and filter can both be implemented in terms of reduce.

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, n) => acc + n, 0);
// sum: 15

Reduce takes an accumulator function and an initial value. The accumulator receives the current accumulated value and each element, returning the new accumulated value.

Real-World Reduce Examples

// grouping
const people = [
    { name: "Alice", age: 30 },
    { name: "Bob", age: 25 },
    { name: "Charlie", age: 30 }
];

const groupedByAge = people.reduce((acc, person) => {
    const key = person.age;
    if (!acc[key]) acc[key] = [];
    acc[key].push(person);
    return acc;
---, {});
// { 25: [{ name: "Bob" }], 30: [{ name: "Alice" }, { name: "Charlie" }] }

// counting
const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
const counts = fruits.reduce((acc, fruit) => {
    acc[fruit] = (acc[fruit] || 0) + 1;
    return acc;
---, {});
// { apple: 3, banana: 2, orange: 1 }

Reduce for Building Lookup Maps

const users = [
    { id: 101, name: "Alice" },
    { id: 203, name: "Bob" },
    { id: 305, name: "Charlie" }
];

const byId = users.reduce((map, user) => {
    map[user.id] = user;
    return map;
---, {});
// byId[203] → { id: 203, name: "Bob" }

This pattern is invaluable when you need to look up items by a key in O(1) time.

Function Composition

Function composition is combining simple functions to build more complex ones. In mathematics, composition is written as f(g(x)). In programming, compose functions by passing the output of one as the input to another.

const compose = (f, g) => (x) => f(g(x));

const toUpper = (s) => s.toUpperCase();
const exclaim = (s) => s + "!";
const shout = compose(exclaim, toUpper);

console.log(shout("hello")); // "HELLO!"

Libraries like Ramda and lodash/fp provide compose and pipe utilities. Pipe applies functions left-to-right (top-to-bottom), which many find more readable.

import { pipe } from "lodash/fp";

const processUser = pipe(
    validateEmail,
    normalizeName,
    hashPassword,
    saveToDatabase
);

Currying

Currying transforms a function that takes multiple arguments into a sequence of functions that each take a single argument. This enables partial application — providing some arguments now and others later.

// uncurried
function add(a, b, c) {
    return a + b + c;
---

// curried
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
---

console.log(curriedAdd(1)(2)(3)); // 6

// partial application
const addOne = curriedAdd(1);
const addOneAndTwo = addOne(2);
console.log(addOneAndTwo(3)); // 6

Currying is particularly useful with map, filter, and reduce because it lets you create specialized functions:

const multiply = (a) => (b) => a * b;
const double = multiply(2);
const triple = multiply(3);

[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]

FAQ

What is the difference between map and forEach?

map returns a new array with transformed values. forEach iterates but returns nothing. Use map when you want a transformed collection. Use forEach for side effects like logging.

Can I break out of a reduce loop?

No — reduce processes all elements. Use some, every, find, or findIndex for early termination. For complex scenarios, consider reduce with a sentinel value that skips processing after a condition is met.

Is it better to chain map/filter or use a single reduce?

Chaining is more readable: each step is clearly documented by the function name. Reduce is more performant because it iterates once. For small to medium collections, readability matters more. For large collections with complex transformations, measure both approaches.

Do other languages have map, filter, and reduce?

Yes. Most modern languages support these operations: Python (map, filter, functools.reduce), Java (Stream API), C# (LINQ Select, Where, Aggregate), Rust (.map(), .filter(), .fold()), and Swift (map, filter, reduce).

How do I use reduce to flatten a nested array?

const nested = [[1, 2], [3], [4, 5, 6]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5, 6]

Or use Array.prototype.flat() / flatMap() for flattening by one level.


Related: Explore functional patterns and lazy evaluation.

Higher-Order Functions In Depth

Higher-order functions (HOFs) either take functions as arguments or return functions. Common HOFs: map, filter, reduce, flatMap, forEach, some, every, find, sort, and groupBy. Function composition using compose (right-to-left) and pipe (left-to-right) chains functions. Currying transforms multi-argument functions into chains of single-argument functions. Partial application fixes some arguments. Thunks delay computation. Memoization caches function results. HOFs enable declarative code that expresses intent rather than implementation details.

Performance Considerations

HOFs create new function objects, which can increase GC pressure. Inline closures in hot loops may be less optimized by JIT compilers. Use for loops instead of forEach for performance-critical paths. Lazy evaluation libraries defer computation. Transducers compose map/filter/reduce without intermediate collections.

Practical HOF Patterns in Modern Development

Higher-order functions shine in real-world application code. Debouncing and throttling are classic HOF patterns: const debounce = (fn, delay) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }. Memoization caches expensive function results based on arguments. Middleware patterns in Express.js and Redux use HOFs to compose request processing pipelines. Event handlers in React use curried HOFs for cleaner component props: const handleClick = (id) => (event) => console.log(id, event). Array sorting with a comparator HOF: data.sort((a, b) => customCompare(a, b, options)). Validation pipelines chain predicates with every or some. In testing, HOFs like describe and it in Jest structure test suites declaratively. Error boundaries wrap components with HOFs to catch and handle failures gracefully. The withLogging HOF wraps any function to add structured logging: const withLogging = (fn, name) => (...args) => { console.log(Entering ${name}); const result = fn(...args); console.log(Exiting ${name}); return result; }. This pattern enables cross-cutting concerns like logging, authentication, and rate limiting without modifying core business logic. In functional languages like Haskell and Elm, HOFs are the primary mechanism for code reuse — the standard library is composed almost entirely of higher-order functions that operate on common data types. The asyncMiddleware pattern in Express wraps async route handlers to catch promise rejections: const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next). In Redux, middleware is composed as higher-order functions around the dispatch function, enabling logging, thunks, sagas, and analytics without coupling them to the store implementation.

Related Concepts and Further Reading

Understanding higher order functions requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.

The relationship between higher order functions and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.

For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of higher order functions. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.

Section: Functional Programming 1669 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top