Higher-Order Functions & Functional Programming
Functions Are First-Class Citizens
In JavaScript, functions are first-class citizens. This is one of the most powerful features of the language and the foundation of functional programming in JavaScript. Being "first-class" means that functions are treated exactly like any other value. You can assign them to variables, store them in arrays and objects, pass them as arguments to other functions, and return them from functions. This is fundamentally different from languages where functions are special constructs that can only be declared and called.
Example: Functions as Values
// Assign a function to a variable
const greet = function(name) {
return 'Hello, ' + name + '!';
};
// Store functions in an array
const operations = [
function(a, b) { return a + b; },
function(a, b) { return a - b; },
function(a, b) { return a * b; },
];
// Store functions in an object
const validators = {
isPositive: function(n) { return n > 0; },
isEven: function(n) { return n % 2 === 0; },
isString: function(val) { return typeof val === 'string'; },
};
// Pass a function as an argument
function executeOperation(fn, a, b) {
return fn(a, b);
}
console.log(executeOperation(operations[0], 5, 3)); // 8
console.log(executeOperation(operations[2], 5, 3)); // 15
// Return a function from a function
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30
The createMultiplier function is a factory that returns new functions. Each returned function "remembers" its factor through closure. This pattern of returning functions is the basis of many functional programming techniques we will explore in this lesson.
What Are Higher-Order Functions?
A higher-order function (HOF) is a function that does at least one of the following: accepts one or more functions as arguments, or returns a function as its result. Higher-order functions are the primary mechanism for abstraction in functional programming. They allow you to separate the "what" from the "how" -- you describe what should happen (the callback), and the higher-order function handles the mechanics of how it happens.
Example: Higher-Order Functions Accept and Return Functions
// HOF that accepts a function
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log);
// 0
// 1
// 2
repeat(3, function(i) {
console.log('Iteration ' + (i + 1) + ' of 3');
});
// HOF that returns a function
function unless(test, thenFn) {
return function(...args) {
if (!test(...args)) {
return thenFn(...args);
}
};
}
const logUnlessNegative = unless(
(n) => n < 0,
(n) => console.log(n)
);
logUnlessNegative(5); // 5
logUnlessNegative(-3); // (nothing)
logUnlessNegative(10); // 10
addEventListener, setTimeout, setInterval, Array.prototype.forEach -- all of these accept callback functions as arguments and are therefore higher-order functions.Built-in Higher-Order Functions: map
The map method creates a new array by applying a transformation function to every element of the original array. It does not modify the original array -- it returns a brand new one. This is one of the most frequently used array methods in modern JavaScript and a cornerstone of the functional programming style.
Example: Transforming Data with map
const numbers = [1, 2, 3, 4, 5];
// Double every number
const doubled = numbers.map(function(num) {
return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]
// Arrow function syntax (more common in practice)
const squared = numbers.map(num => num ** 2);
console.log(squared); // [1, 4, 9, 16, 25]
// Transform objects
const users = [
{ firstName: 'Alice', lastName: 'Johnson', age: 28 },
{ firstName: 'Bob', lastName: 'Smith', age: 34 },
{ firstName: 'Carol', lastName: 'Williams', age: 22 },
];
const fullNames = users.map(user => user.firstName + ' ' + user.lastName);
console.log(fullNames);
// ['Alice Johnson', 'Bob Smith', 'Carol Williams']
// map passes three arguments: element, index, array
const withIndex = numbers.map((num, index) => {
return { value: num, position: index };
});
console.log(withIndex);
// [{value:1,position:0}, {value:2,position:1}, ...]
Built-in Higher-Order Functions: filter
The filter method creates a new array containing only the elements that pass a test implemented by the provided function. The callback should return true to keep the element or false to exclude it. Like map, filter does not mutate the original array.
Example: Filtering Data
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
const greaterThanFive = numbers.filter(n => n > 5);
console.log(greaterThanFive); // [6, 7, 8, 9, 10]
// Filter objects
const products = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Phone', price: 699, inStock: false },
{ name: 'Tablet', price: 449, inStock: true },
{ name: 'Monitor', price: 299, inStock: true },
{ name: 'Keyboard', price: 79, inStock: false },
];
const availableProducts = products.filter(p => p.inStock);
console.log(availableProducts.length); // 3
const affordableAvailable = products.filter(p => p.inStock && p.price < 500);
console.log(affordableAvailable);
// [{name:'Tablet',price:449,...}, {name:'Monitor',price:299,...}]
// Remove falsy values from an array
const mixed = [0, 'hello', '', null, 42, undefined, 'world', false, NaN];
const truthy = mixed.filter(Boolean);
console.log(truthy); // ['hello', 42, 'world']
Boolean as the callback to filter is a common pattern for removing all falsy values (0, '', null, undefined, false, NaN) from an array. It works because Boolean(value) returns true for truthy values and false for falsy ones.Built-in Higher-Order Functions: reduce
The reduce method is the most versatile array method. It processes each element of an array and accumulates a single result. The callback receives an accumulator (the running total or result) and the current element, and returns the new accumulator value. You can implement map, filter, and virtually any other array transformation using reduce alone, which is why it is considered the fundamental building block of array processing.
Example: Reducing Arrays to Single Values
const numbers = [1, 2, 3, 4, 5];
// Sum all numbers
const sum = numbers.reduce((accumulator, current) => {
return accumulator + current;
}, 0);
console.log(sum); // 15
// Find the maximum value
const max = numbers.reduce((acc, curr) => {
return curr > acc ? curr : acc;
}, -Infinity);
console.log(max); // 5
// Count occurrences of each item
const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const counts = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});
console.log(counts); // { apple: 3, banana: 2, orange: 1 }
// Group objects by a property
const people = [
{ name: 'Alice', department: 'Engineering' },
{ name: 'Bob', department: 'Marketing' },
{ name: 'Carol', department: 'Engineering' },
{ name: 'Dave', department: 'Marketing' },
{ name: 'Eve', department: 'Design' },
];
const byDepartment = people.reduce((groups, person) => {
const dept = person.department;
if (!groups[dept]) {
groups[dept] = [];
}
groups[dept].push(person);
return groups;
}, {});
console.log(byDepartment);
// { Engineering: [{...}, {...}], Marketing: [{...}, {...}], Design: [{...}] }
// Flatten nested arrays
const nested = [[1, 2], [3, 4], [5, 6]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
console.log(flat); // [1, 2, 3, 4, 5, 6]
reduce. Without it, reduce uses the first element as the initial accumulator and starts iterating from the second element. This can lead to unexpected results, especially with empty arrays where it throws a TypeError. Always provide an initial value to make your code predictable and safe.Built-in Higher-Order Functions: sort
The sort method sorts the elements of an array in place and returns the sorted array. It accepts an optional comparison function. Without a comparison function, sort converts elements to strings and sorts them by UTF-16 code point order, which leads to surprising results with numbers. The comparison function should return a negative number if a should come before b, a positive number if a should come after b, and zero if they are equal.
Example: Sorting with Comparison Functions
// Without comparison function -- sorts as strings!
const nums = [10, 9, 8, 100, 20, 3];
console.log(nums.sort()); // [10, 100, 20, 3, 8, 9] -- WRONG for numbers!
// Correct numeric sort
const sorted = [10, 9, 8, 100, 20, 3].sort((a, b) => a - b);
console.log(sorted); // [3, 8, 9, 10, 20, 100]
// Descending order
const descending = [10, 9, 8, 100, 20, 3].sort((a, b) => b - a);
console.log(descending); // [100, 20, 10, 9, 8, 3]
// Sort objects by property
const students = [
{ name: 'Alice', grade: 92 },
{ name: 'Bob', grade: 85 },
{ name: 'Carol', grade: 98 },
{ name: 'Dave', grade: 78 },
];
const byGrade = [...students].sort((a, b) => b.grade - a.grade);
console.log(byGrade.map(s => s.name));
// ['Carol', 'Alice', 'Bob', 'Dave']
// Sort strings alphabetically (case-insensitive)
const names = ['banana', 'Apple', 'cherry', 'Date'];
const alphabetical = [...names].sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
console.log(alphabetical); // ['Apple', 'banana', 'cherry', 'Date']
map and filter, sort mutates the original array. To avoid mutation, use the spread operator to create a copy first: [...array].sort(compareFn). Alternatively, use Array.prototype.toSorted() which is a newer non-mutating version available in modern JavaScript environments.Chaining Higher-Order Functions
One of the most powerful patterns in functional JavaScript is chaining map, filter, and reduce together. Because map and filter return new arrays, you can call another method on the result immediately. This creates a data processing pipeline that is easy to read and reason about.
Example: Building a Data Pipeline
const transactions = [
{ id: 1, type: 'sale', amount: 250, currency: 'USD' },
{ id: 2, type: 'refund', amount: 50, currency: 'USD' },
{ id: 3, type: 'sale', amount: 175, currency: 'EUR' },
{ id: 4, type: 'sale', amount: 320, currency: 'USD' },
{ id: 5, type: 'refund', amount: 80, currency: 'USD' },
{ id: 6, type: 'sale', amount: 410, currency: 'USD' },
];
// Find total revenue from USD sales only
const totalUsdRevenue = transactions
.filter(t => t.type === 'sale') // keep only sales
.filter(t => t.currency === 'USD') // keep only USD
.map(t => t.amount) // extract amounts
.reduce((sum, amount) => sum + amount, 0); // sum them up
console.log(totalUsdRevenue); // 980
// Transform and summarize data
const summary = transactions
.filter(t => t.currency === 'USD')
.reduce((acc, t) => {
if (t.type === 'sale') acc.sales += t.amount;
if (t.type === 'refund') acc.refunds += t.amount;
return acc;
}, { sales: 0, refunds: 0 });
summary.net = summary.sales - summary.refunds;
console.log(summary); // { sales: 980, refunds: 130, net: 850 }
Creating Custom Higher-Order Functions
Writing your own higher-order functions helps you create reusable abstractions. Instead of repeating similar logic in multiple places, you extract the common pattern into a higher-order function and pass the varying behavior as a callback.
Example: Custom Higher-Order Functions
// Retry a function N times before giving up
function retry(fn, maxAttempts, delay = 1000) {
return async function(...args) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn(...args);
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log('Attempt ' + attempt + ' failed. Retrying...');
await new Promise(r => setTimeout(r, delay));
}
}
};
}
const fetchWithRetry = retry(fetch, 3, 2000);
// fetchWithRetry('https://api.example.com/data')
// Debounce: delay function execution until input stops
function debounce(fn, wait) {
let timeoutId = null;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
}
const handleSearch = debounce(function(query) {
console.log('Searching for: ' + query);
}, 300);
// Throttle: limit function execution to once per interval
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
return fn.apply(this, args);
}
};
}
const handleScroll = throttle(function() {
console.log('Scroll position: ' + window.scrollY);
}, 200);
Function Composition
Function composition is the process of combining two or more functions to produce a new function. The output of one function becomes the input to the next. This is a fundamental concept in functional programming that lets you build complex transformations from simple, reusable pieces.
Example: Composing Functions Manually
// Simple pure functions
const add10 = x => x + 10;
const multiply3 = x => x * 3;
const subtract5 = x => x - 5;
// Manual composition
const transform = x => subtract5(multiply3(add10(x)));
console.log(transform(5)); // subtract5(multiply3(15)) = subtract5(45) = 40
// Reading inside-out is hard. Let us build utilities.
Pipe and Compose Utilities
The pipe and compose utilities automate function composition. pipe applies functions from left to right (the natural reading order), while compose applies them from right to left (the mathematical convention). Both are built using reduce.
Example: Building pipe and compose
// pipe: left-to-right composition
function pipe(...fns) {
return function(initialValue) {
return fns.reduce((value, fn) => fn(value), initialValue);
};
}
// compose: right-to-left composition
function compose(...fns) {
return function(initialValue) {
return fns.reduceRight((value, fn) => fn(value), initialValue);
};
}
const add10 = x => x + 10;
const multiply3 = x => x * 3;
const subtract5 = x => x - 5;
const toString = x => 'Result: ' + x;
// pipe reads naturally: add10, then multiply3, then subtract5, then toString
const processWithPipe = pipe(add10, multiply3, subtract5, toString);
console.log(processWithPipe(5)); // 'Result: 40'
// compose reads mathematically: toString(subtract5(multiply3(add10(x))))
const processWithCompose = compose(toString, subtract5, multiply3, add10);
console.log(processWithCompose(5)); // 'Result: 40'
// Real-world example: text processing pipeline
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, '-');
const removeSpecialChars = str => str.replace(/[^a-z0-9-]/g, '');
const slugify = pipe(trim, toLowerCase, replaceSpaces, removeSpecialChars);
console.log(slugify(' Hello World! This is a Test '));
// 'hello-world-this-is-a-test'
pipe over compose because it reads in the natural left-to-right order. Use pipe for data transformation pipelines and compose when you want to match the mathematical notation f(g(x)). Both produce identical results -- only the argument order differs.Partial Application
Partial application is a technique where you fix (pre-fill) some arguments of a function and return a new function that accepts the remaining arguments. This is different from currying (which transforms a function of N arguments into N functions of 1 argument each). Partial application lets you create specialized versions of general functions.
Example: Partial Application
// Generic partial application utility
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
// General logging function
function log(level, timestamp, message) {
console.log('[' + level + '] ' + timestamp + ': ' + message);
}
// Create specialized loggers
const logError = partial(log, 'ERROR');
const logWarning = partial(log, 'WARNING');
const logInfo = partial(log, 'INFO');
const now = new Date().toISOString();
logError(now, 'Database connection failed');
// [ERROR] 2025-01-15T10:30:00.000Z: Database connection failed
logInfo(now, 'Server started successfully');
// [INFO] 2025-01-15T10:30:00.000Z: Server started successfully
// Using bind for partial application
const multiply = (a, b) => a * b;
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Partial application with configuration
function createApiClient(baseUrl, headers) {
return function(endpoint) {
return fetch(baseUrl + endpoint, { headers });
};
}
const githubApi = createApiClient('https://api.github.com', {
'Accept': 'application/vnd.github.v3+json',
});
// Later, just pass the endpoint
// githubApi('/users/octocat')
Memoization
Memoization is a functional programming optimization technique that caches the results of expensive function calls. When a memoized function is called with the same arguments again, it returns the cached result instead of recomputing. This only works correctly with pure functions (functions that always return the same output for the same input and have no side effects).
Example: Implementing Memoization
// Generic memoize utility
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit for: ' + key);
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Expensive computation
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Without memoization: fibonacci(40) takes several seconds
// With memoization: nearly instant after first call
const memoFib = memoize(function fib(n) {
if (n <= 1) return n;
return memoFib(n - 1) + memoFib(n - 2);
});
console.time('memoized');
console.log(memoFib(40)); // 102334155
console.timeEnd('memoized'); // ~1ms
// Memoize API calls
const memoizedFetch = memoize(async function(url) {
const response = await fetch(url);
return response.json();
});
// Memoize with limited cache size (LRU-style)
function memoizeWithLimit(fn, maxSize = 100) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const value = cache.get(key);
// Move to end (most recently used)
cache.delete(key);
cache.set(key, value);
return value;
}
const result = fn.apply(this, args);
cache.set(key, result);
// Evict oldest entry if over limit
if (cache.size > maxSize) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
return result;
};
}
Pure Functions
A pure function is a function that satisfies two conditions: it always returns the same output for the same input (deterministic), and it produces no side effects (it does not modify external state, perform I/O, or change its arguments). Pure functions are the building blocks of functional programming because they are predictable, testable, cacheable (memoizable), and safe to run in parallel.
Example: Pure vs Impure Functions
// PURE: same input always produces same output, no side effects
function add(a, b) {
return a + b;
}
function calculateTax(price, rate) {
return price * rate;
}
function formatName(first, last) {
return first.charAt(0).toUpperCase() + first.slice(1) + ' ' +
last.charAt(0).toUpperCase() + last.slice(1);
}
// IMPURE: depends on external state
let discount = 0.1;
function calculatePrice(amount) {
return amount * (1 - discount); // depends on external `discount`
}
// IMPURE: modifies external state (side effect)
let total = 0;
function addToTotal(amount) {
total += amount; // mutates external variable
return total;
}
// IMPURE: modifies its input (side effect)
function addItem(cart, item) {
cart.push(item); // mutates the cart array
return cart;
}
// PURE version of addItem
function addItemPure(cart, item) {
return [...cart, item]; // returns a new array
}
// IMPURE: performs I/O
function logAndReturn(value) {
console.log(value); // side effect: I/O
return value;
}
Immutability Patterns
Immutability means not changing data after it is created. Instead of modifying existing objects or arrays, you create new ones with the desired changes. This prevents bugs caused by shared mutable state and makes your code more predictable. JavaScript provides several tools for working with immutable data.
Example: Immutability Techniques
// Object.freeze: prevents modification of an object (shallow)
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
});
config.timeout = 10000; // silently fails (throws in strict mode)
console.log(config.timeout); // still 5000
// CAUTION: Object.freeze is shallow
const user = Object.freeze({
name: 'Alice',
preferences: { theme: 'dark', language: 'en' },
});
user.preferences.theme = 'light'; // This WORKS -- nested objects are NOT frozen
console.log(user.preferences.theme); // 'light'
// Deep freeze utility
function deepFreeze(obj) {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null && !Object.isFrozen(obj[key])) {
deepFreeze(obj[key]);
}
});
return obj;
}
// Spread operator for immutable updates
const original = { name: 'Alice', age: 28, city: 'NYC' };
const updated = { ...original, age: 29 }; // new object, age changed
console.log(original.age); // 28 (unchanged)
console.log(updated.age); // 29
// Immutable array operations
const numbers = [1, 2, 3, 4, 5];
// Instead of push, use spread
const withSix = [...numbers, 6]; // [1, 2, 3, 4, 5, 6]
// Instead of splice, use filter
const withoutThree = numbers.filter(n => n !== 3); // [1, 2, 4, 5]
// Instead of modifying by index, use map
const doubleThird = numbers.map((n, i) => i === 2 ? n * 2 : n);
// [1, 2, 6, 4, 5]
// structuredClone for deep copies (modern JavaScript)
const complex = {
users: [
{ name: 'Alice', scores: [95, 87, 92] },
{ name: 'Bob', scores: [78, 85, 90] },
],
metadata: { version: 2, lastUpdated: new Date() },
};
const deepCopy = structuredClone(complex);
deepCopy.users[0].scores.push(100);
console.log(complex.users[0].scores.length); // 3 (unchanged)
console.log(deepCopy.users[0].scores.length); // 4
= to "copy" objects or arrays creates a reference, not a copy. Both variables point to the same data in memory. Modifying one modifies the other. Always use spread syntax {...obj}, [...arr], or structuredClone() for true copies. Use structuredClone when you have nested objects that need deep copying.Declarative vs Imperative Style
Imperative programming describes how to do something step by step. Declarative programming describes what you want the result to be, leaving the implementation details to abstracted functions. Functional programming favors the declarative style because it is more readable, concise, and less error-prone.
Example: Imperative vs Declarative
const orders = [
{ product: 'Laptop', quantity: 2, unitPrice: 999 },
{ product: 'Mouse', quantity: 5, unitPrice: 25 },
{ product: 'Monitor', quantity: 1, unitPrice: 450 },
{ product: 'Keyboard', quantity: 3, unitPrice: 75 },
{ product: 'Webcam', quantity: 2, unitPrice: 89 },
];
// IMPERATIVE: step-by-step instructions
let imperativeResult = [];
for (let i = 0; i < orders.length; i++) {
const total = orders[i].quantity * orders[i].unitPrice;
if (total > 200) {
imperativeResult.push({
product: orders[i].product,
total: total,
});
}
}
imperativeResult.sort(function(a, b) {
return b.total - a.total;
});
// DECLARATIVE: describe what you want
const declarativeResult = orders
.map(order => ({
product: order.product,
total: order.quantity * order.unitPrice,
}))
.filter(order => order.total > 200)
.sort((a, b) => b.total - a.total);
console.log(declarativeResult);
// [
// { product: 'Laptop', total: 1998 },
// { product: 'Monitor', total: 450 },
// { product: 'Keyboard', total: 225 },
// ]
The declarative version reads like a description of the transformation: "map each order to a product and total, filter those above 200, sort by total descending." The imperative version requires you to mentally trace through loop variables, conditionals, and mutations to understand the result.
Practical Functional Refactoring
Let us take a real-world imperative function and refactor it step by step into a clean, functional style. This demonstrates how functional programming improves code readability and maintainability in practice.
Example: Refactoring Imperative to Functional
// BEFORE: Imperative user processing
function processUsersImperative(users) {
let result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
let fullName = users[i].firstName + ' ' + users[i].lastName;
let user = {
id: users[i].id,
name: fullName.toUpperCase(),
email: users[i].email.toLowerCase(),
yearsSinceJoining: new Date().getFullYear() - users[i].joinYear,
};
if (user.yearsSinceJoining >= 2) {
result.push(user);
}
}
}
result.sort(function(a, b) {
return b.yearsSinceJoining - a.yearsSinceJoining;
});
return result;
}
// AFTER: Functional refactoring with small, reusable functions
const isActive = user => user.active;
const toDisplayUser = user => ({
id: user.id,
name: (user.firstName + ' ' + user.lastName).toUpperCase(),
email: user.email.toLowerCase(),
yearsSinceJoining: new Date().getFullYear() - user.joinYear,
});
const hasMinYears = minYears => user => user.yearsSinceJoining >= minYears;
const byYearsDescending = (a, b) => b.yearsSinceJoining - a.yearsSinceJoining;
function processUsersFunctional(users) {
return users
.filter(isActive)
.map(toDisplayUser)
.filter(hasMinYears(2))
.sort(byYearsDescending);
}
// Test with sample data
const users = [
{ id: 1, firstName: 'alice', lastName: 'johnson', email: 'Alice@Email.COM', joinYear: 2020, active: true },
{ id: 2, firstName: 'bob', lastName: 'smith', email: 'Bob@Work.COM', joinYear: 2024, active: true },
{ id: 3, firstName: 'carol', lastName: 'williams', email: 'Carol@Mail.COM', joinYear: 2019, active: false },
{ id: 4, firstName: 'dave', lastName: 'brown', email: 'Dave@Corp.COM', joinYear: 2021, active: true },
];
console.log(processUsersFunctional(users));
// [
// { id: 1, name: 'ALICE JOHNSON', email: 'alice@email.com', yearsSinceJoining: 5 },
// { id: 4, name: 'DAVE BROWN', email: 'dave@corp.com', yearsSinceJoining: 4 },
// ]
Notice how the functional version breaks the problem into small, named functions. Each function does one thing: isActive filters active users, toDisplayUser transforms the shape, hasMinYears creates a tenure filter (using partial application), and byYearsDescending defines the sort order. Each of these is independently testable and reusable. The processUsersFunctional function reads like a description of the pipeline: filter active users, transform to display format, filter by minimum years, sort by years descending.
Example: Functional Composition in a Real Application
// Building a validation pipeline using composition
const createValidator = (rules) => (value) => {
const errors = rules
.map(rule => rule(value))
.filter(error => error !== null);
return {
isValid: errors.length === 0,
errors,
};
};
// Individual validation rules (pure functions)
const required = (value) =>
value === '' || value === null || value === undefined
? 'This field is required'
: null;
const minLength = (min) => (value) =>
typeof value === 'string' && value.length < min
? 'Must be at least ' + min + ' characters'
: null;
const maxLength = (max) => (value) =>
typeof value === 'string' && value.length > max
? 'Must be no more than ' + max + ' characters'
: null;
const matchesPattern = (regex, message) => (value) =>
typeof value === 'string' && !regex.test(value)
? message
: null;
// Compose validators for specific fields
const validateUsername = createValidator([
required,
minLength(3),
maxLength(20),
matchesPattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed'),
]);
const validateEmail = createValidator([
required,
matchesPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email format'),
]);
console.log(validateUsername('ab'));
// { isValid: false, errors: ['Must be at least 3 characters'] }
console.log(validateUsername('alice_smith'));
// { isValid: true, errors: [] }
console.log(validateEmail('not-an-email'));
// { isValid: false, errors: ['Invalid email format'] }
This validation system demonstrates several functional programming principles working together: pure functions as validation rules, partial application for configurable rules (like minLength(3)), higher-order functions to create the validator, and map plus filter for processing. Each validation rule is a small, testable, reusable function that composes cleanly into larger validation pipelines.
map/filter/reduce over imperative loops, avoid mutating function arguments, and use composition to build complex behavior from simple parts. Even partial adoption of functional patterns will make your code significantly more maintainable.Practice Exercise
Build a complete data processing module using functional programming techniques. Start with this array of employee data:
const employees = [
{ id: 1, name: 'Alice', department: 'Engineering', salary: 95000, performance: 4.5, yearsExp: 7 },
{ id: 2, name: 'Bob', department: 'Marketing', salary: 72000, performance: 3.8, yearsExp: 4 },
{ id: 3, name: 'Carol', department: 'Engineering', salary: 110000, performance: 4.9, yearsExp: 10 },
{ id: 4, name: 'Dave', department: 'Design', salary: 85000, performance: 4.2, yearsExp: 6 },
{ id: 5, name: 'Eve', department: 'Engineering', salary: 88000, performance: 3.5, yearsExp: 3 },
{ id: 6, name: 'Frank', department: 'Marketing', salary: 68000, performance: 4.0, yearsExp: 5 },
{ id: 7, name: 'Grace', department: 'Design', salary: 92000, performance: 4.7, yearsExp: 8 },
];
Complete the following tasks using only functional programming techniques (no loops, no mutation): (1) Create a pipe function. (2) Write pure filter functions: byDepartment(dept), byMinPerformance(min), byMinExperience(years). (3) Write a pure transform function calculateBonus that adds a bonus field equal to salary times performance divided by 5. (4) Use pipe to create a pipeline that filters Engineering employees with performance above 4.0, calculates their bonus, and sorts by bonus descending. (5) Write a memoize function and wrap your pipeline in it. (6) Create a groupBy higher-order function using reduce and use it to group all employees by department. (7) Write a compose function and use it with your pure functions to create a reusable salary report generator that takes employees, filters by minimum experience of 5 years, maps to objects with name, department, and salary, and sorts by salary descending.