Advanced Higher-Order Functions: The Art of Function Metaprogramming
Beyond Basic Higher-Order Functions
If basic higher-order functions like map, filter, and reduce are the toolbox of functional programming, then advanced higher-order functions are the tools that build tools—they don't just process data, they process functions themselves.
In the world of JavaScript, functions are first-class citizens. This means functions can not only be passed as arguments and returned as values, but also be stored, modified, and composed. Advanced higher-order functions fully leverage this characteristic to create powerful and elegant abstractions.
Function Decorators
A decorator is a special type of higher-order function that receives a function and returns an enhanced version of that function without changing its core behavior.
1. Logging Decorator
// Basic logging decorator
function withLogging(fn, label = fn.name) {
return function (...args) {
console.log(`[${label}] Called with args:`, args);
const result = fn(...args);
console.log(`[${label}] Returned:`, result);
return result;
};
}
// Using the decorator
function add(a, b) {
return a + b;
}
const addWithLogging = withLogging(add, "ADD");
addWithLogging(5, 3);
// [ADD] Called with args: [5, 3]
// [ADD] Returned: 8
// => 82. Performance Timing Decorator
function withTiming(fn, label = fn.name) {
return function (...args) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`[${label}] Execution time: ${(end - start).toFixed(2)}ms`);
return result;
};
}
// Measure function performance
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const timedFib = withTiming(fibonacci, "Fibonacci");
console.log(timedFib(30));
// [Fibonacci] Execution time: 12.34ms
// => 8320403. Error Handling Decorator
function withErrorHandling(fn, onError = console.error) {
return function (...args) {
try {
return fn(...args);
} catch (error) {
onError(`Function ${fn.name} execution error:`, error.message);
return null;
}
};
}
// Usage example
function parseJSON(jsonString) {
return JSON.parse(jsonString);
}
const safeParseJSON = withErrorHandling(parseJSON);
console.log(safeParseJSON('{"name":"John"}')); // { name: 'John' }
console.log(safeParseJSON("{invalid}")); // null (error caught)4. Retry Decorator
function withRetry(fn, maxAttempts = 3, 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 in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
};
}
// Simulate unstable API call
async function fetchData(url) {
if (Math.random() < 0.7) {
// 70% failure rate
throw new Error("Network error");
}
return { data: `Data from ${url}` };
}
const reliableFetch = withRetry(fetchData, 3, 500);
reliableFetch("https://api.example.com/data")
.then((result) => console.log("Success:", result))
.catch((error) => console.log("Final failure:", error.message));5. Combining Multiple Decorators
// Compose decorators
function compose(...decorators) {
return function (fn) {
return decorators.reduceRight((decorated, decorator) => {
return decorator(decorated);
}, fn);
};
}
// Create a fully decorated function
function expensiveCalculation(n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += Math.sqrt(i);
}
return result;
}
const fullyDecorated = compose(
withLogging,
withTiming,
withErrorHandling
)(expensiveCalculation);
fullyDecorated(1000000);
// [expensiveCalculation] Called with args: [1000000]
// [expensiveCalculation] Execution time: 25.67ms
// [expensiveCalculation] Returned: 666666166.6667Partial Application
Partial application pre-fills some parameters of a function, creating a new function with fewer parameters.
1. Basic Partial Application
// General partial application
function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
// Example: Mathematical operations
function multiply(a, b, c) {
return a * b * c;
}
const multiplyByTwo = partial(multiply, 2);
console.log(multiplyByTwo(3, 4)); // 2 * 3 * 4 = 24
const multiplyByTwoAndThree = partial(multiply, 2, 3);
console.log(multiplyByTwoAndThree(4)); // 2 * 3 * 4 = 242. Real-world Application: HTTP Requests
// General fetch wrapper
async function request(baseURL, headers, method, endpoint, body) {
const url = `${baseURL}${endpoint}`;
const options = {
method,
headers: { ...headers, "Content-Type": "application/json" },
...(body && { body: JSON.stringify(body) }),
};
const response = await fetch(url, options);
return response.json();
}
// Create API client
const apiRequest = partial(request, "https://api.example.com", {
Authorization: "Bearer token123",
});
const get = partial(apiRequest, "GET");
const post = partial(apiRequest, "POST");
const put = partial(apiRequest, "PUT");
const del = partial(apiRequest, "DELETE");
// Usage
get("/users"); // GET /users
post("/users", { name: "John" }); // POST /users
put("/users/1", { name: "Jane" }); // PUT /users/1
del("/users/1"); // DELETE /users/13. Configuration-based Partial Application
// Data validator
function createValidator(rules, errorMessages, data) {
const errors = [];
for (const [field, rule] of Object.entries(rules)) {
if (!rule(data[field])) {
errors.push({
field,
message: errorMessages[field] || `${field} validation failed`,
});
}
}
return {
isValid: errors.length === 0,
errors,
};
}
// Pre-set validation rules and error messages
const validateUser = partial(
createValidator,
{
name: (value) => value && value.length >= 2,
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
age: (value) => value >= 18 && value <= 100,
},
{
name: "Name must be at least 2 characters",
email: "Please enter a valid email address",
age: "Age must be between 18-100",
}
);
// Usage
console.log(validateUser({ name: "J", email: "invalid", age: 15 }));
// { isValid: false, errors: [...] }
console.log(
validateUser({
name: "John",
email: "[email protected]",
age: 25,
})
);
// { isValid: true, errors: [] }Function Combinators
Combinators are higher-order functions that operate on functions to build complex data processing pipelines.
1. Compose and Pipe
// compose: Execute from right to left
const compose =
(...fns) =>
(value) =>
fns.reduceRight((acc, fn) => fn(acc), value);
// pipe: Execute from left to right
const pipe =
(...fns) =>
(value) =>
fns.reduce((acc, fn) => fn(acc), value);
// Data transformation functions
const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const addExclamation = (str) => str + "!";
// Using pipe (more intuitive)
const formatGreeting = pipe(trim, toLowerCase, capitalize, addExclamation);
console.log(formatGreeting(" HELLO ")); // "Hello!"2. Async Composition
// Async pipe
const asyncPipe =
(...fns) =>
(value) =>
fns.reduce(async (acc, fn) => fn(await acc), value);
// Async operations
const fetchUser = async (id) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return { id, name: "John", age: 25 };
};
const enrichWithPosts = async (user) => {
await new Promise((resolve) => setTimeout(resolve, 100));
return { ...user, posts: ["Post 1", "Post 2"] };
};
const addTimestamp = async (user) => {
return { ...user, fetchedAt: new Date() };
};
// Compose async operations
const getUserWithData = asyncPipe(fetchUser, enrichWithPosts, addTimestamp);
getUserWithData(1).then(console.log);
// { id: 1, name: 'John', age: 25, posts: [...], fetchedAt: ... }3. Conditional Composition
// Conditional execution combinator
const when = (predicate, fn) => (value) => predicate(value) ? fn(value) : value;
const unless = (predicate, fn) => when((value) => !predicate(value), fn);
// Usage example
const processNumber = pipe(
when((n) => n < 0, Math.abs), // Convert to positive
when(
(n) => n > 100,
(n) => 100
), // Limit maximum
unless(Number.isInteger, Math.floor) // Take integer part
);
console.log(processNumber(-50)); // 50
console.log(processNumber(150)); // 100
console.log(processNumber(45.7)); // 45
console.log(processNumber(30)); // 304. Map Combinator
// Map combinator - Apply function to each element of array
const map = (fn) => (array) => array.map(fn);
// Compose multiple mapping operations
const processUsers = pipe(
map((user) => ({ ...user, name: user.name.toUpperCase() })),
map((user) => ({ ...user, isAdult: user.age >= 18 })),
map((user) => ({
...user,
category: user.age < 18 ? "youth" : user.age < 60 ? "adult" : "senior",
}))
);
const users = [
{ name: "john", age: 15 },
{ name: "jane", age: 25 },
{ name: "bob", age: 65 },
];
console.log(processUsers(users));Memoization Decorators
Memoization optimizes performance by caching function results:
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("Return from cache:", key);
return cache.get(key);
}
console.log("Calculate new result:", key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// Fibonacci sequence - unoptimized
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Memoized version
const memoizedFib = memoize(fibonacci);
console.time("First call");
console.log(memoizedFib(40));
console.timeEnd("First call");
console.time("Second call");
console.log(memoizedFib(40));
console.timeEnd("Second call");
// Second call completes almost instantlyAdvanced memoization - Support custom caching strategies:
function memoizeAdvanced(fn, options = {}) {
const {
maxSize = Infinity,
maxAge = Infinity,
keyGenerator = JSON.stringify,
} = options;
const cache = new Map();
const timestamps = new Map();
return function (...args) {
const key = keyGenerator(args);
const now = Date.now();
// Check cache
if (cache.has(key)) {
const timestamp = timestamps.get(key);
// Check if expired
if (now - timestamp < maxAge) {
return cache.get(key);
}
// Delete if expired
cache.delete(key);
timestamps.delete(key);
}
// Calculate result
const result = fn(...args);
// Check cache size limit
if (cache.size >= maxSize) {
// Delete oldest entry
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
timestamps.delete(firstKey);
}
// Store in cache
cache.set(key, result);
timestamps.set(key, now);
return result;
};
}
// Usage example: API call caching
const fetchUserData = memoizeAdvanced(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
{
maxSize: 100, // Cache at most 100 users
maxAge: 60000, // Cache for 1 minute
keyGenerator: ([userId]) => `user_${userId}`,
}
);Function Factories
Function factories are higher-order functions that return configured functions:
// Create validator factory
function createValidator(type) {
const validators = {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
phone: (value) => /^\d{10,11}$/.test(value),
url: (value) => /^https?:\/\/.+/.test(value),
required: (value) => value != null && value !== "",
minLength: (min) => (value) => value.length >= min,
maxLength: (max) => (value) => value.length <= max,
range: (min, max) => (value) => value >= min && value <= max,
};
return validators[type];
}
// Usage
const isEmail = createValidator("email");
const isPhone = createValidator("phone");
const isMinLength8 = createValidator("minLength")(8);
console.log(isEmail("[email protected]")); // true
console.log(isPhone("1234567890")); // true
console.log(isMinLength8("password123")); // trueCreate data transformer factory:
function createTransformer(type) {
const transformers = {
trim: (str) => str.trim(),
uppercase: (str) => str.toUpperCase(),
lowercase: (str) => str.toLowerCase(),
capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
slugify: (str) =>
str
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-"),
truncate: (maxLength) => (str) =>
str.length > maxLength ? str.slice(0, maxLength) + "..." : str,
padStart:
(length, char = " ") =>
(str) =>
str.padStart(length, char),
padEnd:
(length, char = " ") =>
(str) =>
str.padEnd(length, char),
};
return transformers[type];
}
// Compose transformers
const processTitle = pipe(
createTransformer("trim"),
createTransformer("capitalize"),
createTransformer("truncate")(50)
);
console.log(
processTitle(" this is a very long title that needs processing ")
);Real-world Application: Middleware System
// Express-style middleware system
class MiddlewareEngine {
constructor() {
this.middlewares = [];
}
use(fn) {
this.middlewares.push(fn);
return this;
}
async execute(context) {
let index = 0;
const next = async () => {
if (index >= this.middlewares.length) {
return;
}
const middleware = this.middlewares[index++];
await middleware(context, next);
};
await next();
return context;
}
}
// Usage example
const app = new MiddlewareEngine();
// Logging middleware
app.use(async (ctx, next) => {
console.log("-> Request start:", ctx.path);
await next();
console.log("<- Request end:", ctx.path);
});
// Authentication middleware
app.use(async (ctx, next) => {
if (!ctx.user) {
ctx.error = "Not authenticated";
return;
}
await next();
});
// Business logic
app.use(async (ctx, next) => {
ctx.result = `Processing ${ctx.path}`;
await next();
});
// Execute
app
.execute({
path: "/api/users",
user: { id: 1, name: "John" },
})
.then((result) => {
console.log("Final result:", result);
});Summary
Advanced higher-order functions are powerful tools in functional programming that enable us to:
- Decorator Pattern: Enhance function functionality without modifying the original function
- Partial Application: Create function variants with preset parameters
- Function Composition: Build complex data processing pipelines
- Memoization: Optimize performance through caching
- Function Factories: Generate configured functions
These techniques not only make code more concise and elegant but also provide powerful abstraction capabilities. Mastering advanced higher-order functions allows you to accomplish more work with less code and write more maintainable and testable programs.
The key to higher-order functions is treating functions as data—they can be created, passed, composed, and transformed. This mindset will completely change how you build programs.