Lesson 4 of 8

Higher-Order Functions

Functions that take or return other functions - map, filter, reduce and beyond

What are Higher-Order Functions?

A Higher-Order Function (HOF) is a function that either takes one or more functions as arguments, returns a function, or both. HOFs are fundamental to functional programming and enable powerful abstractions.

// Takes a function as argument
function doTwice(fn, x) {
  return fn(fn(x));
}
doTwice(x => x * 2, 5); // 20

// Returns a function
function multiplier(factor) {
  return x => x * factor;
}
const double = multiplier(2);
double(5); // 10

// Both
function compose(f, g) {
  return x => f(g(x));
}
const addOneThenDouble = compose(x => x * 2, x => x + 1);
addOneThenDouble(5); // 12

The Big Three: map, filter, reduce

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

// MAP: Transform each element
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]

// FILTER: Keep elements that pass a test
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]

// REDUCE: Combine all elements into one value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15

// Chaining them together
const result = [1, 2, 3, 4, 5]
  .filter(n => n % 2 === 0)    // [2, 4]
  .map(n => n * 10)            // [20, 40]
  .reduce((acc, n) => acc + n, 0); // 60

Deep Dive: map

// map applies a function to each element
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 },
];

// Extract property
const names = users.map(user => user.name);
// ['Alice', 'Bob', 'Charlie']

// Transform objects
const withBirthYear = users.map(user => ({
  ...user,
  birthYear: 2024 - user.age,
}));

// With index
const indexed = users.map((user, index) => ({
  ...user,
  id: index + 1,
}));

// Implementing map ourselves
function myMap(arr, fn) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(fn(arr[i], i, arr));
  }
  return result;
}

Deep Dive: filter

const products = [
  { name: 'Laptop', price: 999, inStock: true },
  { name: 'Phone', price: 699, inStock: false },
  { name: 'Tablet', price: 449, inStock: true },
  { name: 'Watch', price: 299, inStock: true },
];

// Filter by condition
const available = products.filter(p => p.inStock);
const expensive = products.filter(p => p.price > 500);
const affordableInStock = products.filter(p => p.inStock && p.price < 500);

// Remove falsy values
const mixed = [0, 'hello', '', null, 42, undefined, 'world'];
const truthy = mixed.filter(Boolean);
// ['hello', 42, 'world']

// Remove duplicates (with Set is better, but filter can do it)
const nums = [1, 2, 2, 3, 3, 3, 4];
const unique = nums.filter((n, i, arr) => arr.indexOf(n) === i);
// [1, 2, 3, 4]

// Implementing filter ourselves
function myFilter(arr, predicate) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (predicate(arr[i], i, arr)) {
      result.push(arr[i]);
    }
  }
  return result;
}

Deep Dive: reduce

// reduce is the most powerful HOF - can implement map and filter!
const numbers = [1, 2, 3, 4, 5];

// Sum
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15

// Product
const product = numbers.reduce((acc, n) => acc * n, 1); // 120

// Find max
const max = numbers.reduce((a, b) => a > b ? a : b);

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

// Group by property
const people = [
  { name: 'Alice', city: 'NYC' },
  { name: 'Bob', city: 'LA' },
  { name: 'Charlie', city: 'NYC' },
];
const byCity = people.reduce((acc, person) => ({
  ...acc,
  [person.city]: [...(acc[person.city] || []), person],
}), {});
// { NYC: [{Alice}, {Charlie}], LA: [{Bob}] }

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

// Implement map with reduce
const mapWithReduce = (arr, fn) =>
  arr.reduce((acc, x, i) => [...acc, fn(x, i)], []);

// Implement filter with reduce
const filterWithReduce = (arr, pred) =>
  arr.reduce((acc, x, i) => pred(x, i) ? [...acc, x] : acc, []);

Other Useful HOFs

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

// find - returns first match
const firstEven = numbers.find(n => n % 2 === 0); // 2

// findIndex - returns index of first match
const firstEvenIndex = numbers.findIndex(n => n % 2 === 0); // 1

// some - returns true if any element passes
const hasEven = numbers.some(n => n % 2 === 0); // true

// every - returns true if all elements pass
const allPositive = numbers.every(n => n > 0); // true

// flatMap - map + flatten
const sentences = ['Hello world', 'Foo bar'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'Foo', 'bar']

// forEach - side effects (not pure FP, but useful)
numbers.forEach(n => console.log(n));

Creating Your Own HOFs

// Function factory
function createValidator(rules) {
  return value => rules.every(rule => rule(value));
}

const isValidPassword = createValidator([
  pwd => pwd.length >= 8,
  pwd => /[A-Z]/.test(pwd),
  pwd => /[0-9]/.test(pwd),
]);

isValidPassword('Abc12345'); // true
isValidPassword('weak');     // false

// Debounce - classic HOF
function debounce(fn, delay) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

const debouncedSearch = debounce(query => {
  console.log('Searching:', query);
}, 300);

// Memoize - cache function results
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize(n => {
  console.log('Computing...');
  return n * 100;
});

expensiveCalc(5); // 'Computing...' → 500
expensiveCalc(5); // → 500 (cached, no log)

// Once - run only once
function once(fn) {
  let called = false;
  let result;
  return (...args) => {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}

const initialize = once(() => console.log('Initialized!'));
initialize(); // 'Initialized!'
initialize(); // (nothing)