Skip to content

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

javascript
// 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
// => 8

2. Performance Timing Decorator

javascript
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
// => 832040

3. Error Handling Decorator

javascript
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

javascript
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

javascript
// 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.6667

Partial Application

Partial application pre-fills some parameters of a function, creating a new function with fewer parameters.

1. Basic Partial Application

javascript
// 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 = 24

2. Real-world Application: HTTP Requests

javascript
// 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/1

3. Configuration-based Partial Application

javascript
// 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

javascript
// 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

javascript
// 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

javascript
// 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)); // 30

4. Map Combinator

javascript
// 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:

javascript
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 instantly

Advanced memoization - Support custom caching strategies:

javascript
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:

javascript
// 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")); // true

Create data transformer factory:

javascript
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

javascript
// 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:

  1. Decorator Pattern: Enhance function functionality without modifying the original function
  2. Partial Application: Create function variants with preset parameters
  3. Function Composition: Build complex data processing pipelines
  4. Memoization: Optimize performance through caching
  5. 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.