Learning Objectives

  • Understand function composition principles
  • Create compose and pipe utilities
  • Build data transformation pipelines
  • Apply composition to real-world problems

What Is Function Composition?

Function composition is the process of combining simple functions to build more complex ones. It's like connecting pipes where the output of one function becomes the input of the next.

// Simple functions
const add5 = x => x + 5;
const multiply3 = x => x * 3;
const subtract2 = x => x - 2;

// Manual composition
const result = subtract2(multiply3(add5(10)));
console.log(result); // 43
// Steps: 10 → 15 → 45 → 43

The compose Function

Create a utility that composes functions right-to-left:

function compose(...fns) {
    return function(value) {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

// Use it
const calculate = compose(
    subtract2,
    multiply3,
    add5
);

console.log(calculate(10)); // 43
// Reads right-to-left: add5 → multiply3 → subtract2

The pipe Function

Create a utility that composes functions left-to-right (more intuitive):

function pipe(...fns) {
    return function(value) {
        return fns.reduce((acc, fn) => fn(acc), value);
    };
}

// Use it
const calculate = pipe(
    add5,
    multiply3,
    subtract2
);

console.log(calculate(10)); // 43
// Reads left-to-right: add5 → multiply3 → subtract2

Practical Example: Data Transformation

// Data transformation functions
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const removeSpaces = str => str.replace(/\s+/g, '-');
const addPrefix = prefix => str => `${prefix}${str}`;
const addSuffix = suffix => str => `${str}${suffix}`;

// Create a slug generator
const createSlug = pipe(
    trim,
    toLowerCase,
    removeSpaces,
    addPrefix('blog-'),
    addSuffix('-2024')
);

console.log(createSlug('  Hello World  '));
// Output: "blog-hello-world-2024"

Practical Example: Validation Pipeline

// Validation functions
const isNotEmpty = value => {
    if (!value || value.trim() === '') {
        throw new Error('Value cannot be empty');
    }
    return value;
};

const isEmail = value => {
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        throw new Error('Invalid email format');
    }
    return value;
};

const isLongEnough = min => value => {
    if (value.length < min) {
        throw new Error(`Value must be at least ${min} characters`);
    }
    return value;
};

const normalizeEmail = value => value.toLowerCase().trim();

// Create email validator
const validateEmail = pipe(
    isNotEmpty,
    normalizeEmail,
    isLongEnough(5),
    isEmail
);

try {
    const email = validateEmail('  Alice@Example.COM  ');
    console.log('Valid email:', email); // "alice@example.com"
} catch (error) {
    console.error(error.message);
}

Practical Example: Array Processing

// Array transformation functions
const filterAdults = users => users.filter(u => u.age >= 18);
const sortByAge = users => [...users].sort((a, b) => a.age - b.age);
const extractNames = users => users.map(u => u.name);
const joinWithComma = names => names.join(', ');

// Create user processor
const processUsers = pipe(
    filterAdults,
    sortByAge,
    extractNames,
    joinWithComma
);

const users = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 17 },
    { name: 'Charlie', age: 30 },
    { name: 'David', age: 16 },
    { name: 'Eve', age: 22 }
];

console.log(processUsers(users));
// Output: "Eve, Alice, Charlie"

Composing with Closures

Closures make composition even more powerful by allowing configuration:

// Configurable transformations
const multiply = factor => value => value * factor;
const add = amount => value => value + amount;
const power = exponent => value => Math.pow(value, exponent);
const round = decimals => value => {
    const multiplier = Math.pow(10, decimals);
    return Math.round(value * multiplier) / multiplier;
};

// Create specialized calculators
const calculatePrice = pipe(
    multiply(1.2),      // Add 20% markup
    add(5),             // Add $5 shipping
    multiply(1.08),     // Add 8% tax
    round(2)            // Round to 2 decimals
);

console.log(calculatePrice(100)); // 133.60

// Create different calculator
const calculateDiscount = pipe(
    multiply(0.8),      // 20% discount
    round(2)
);

console.log(calculateDiscount(100)); // 80.00

Async Composition

Compose asynchronous functions:

function pipeAsync(...fns) {
    return async function(value) {
        let result = value;
        for (const fn of fns) {
            result = await fn(result);
        }
        return result;
    };
}

// Async transformation functions
const fetchUser = async id => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
};

const enrichUserData = async user => {
    const orders = await fetch(`/api/orders?userId=${user.id}`).then(r => r.json());
    return { ...user, orders };
};

const calculateTotalSpent = user => {
    const total = user.orders.reduce((sum, order) => sum + order.total, 0);
    return { ...user, totalSpent: total };
};

const formatUserReport = user => ({
    name: user.name,
    email: user.email,
    orderCount: user.orders.length,
    totalSpent: `$${user.totalSpent.toFixed(2)}`
});

// Create user report generator
const generateUserReport = pipeAsync(
    fetchUser,
    enrichUserData,
    calculateTotalSpent,
    formatUserReport
);

// Use it
generateUserReport(123).then(report => console.log(report));

Practical Example: Middleware Pattern

// Middleware functions
const logger = next => req => {
    console.log(`${req.method} ${req.url}`);
    return next(req);
};

const auth = next => req => {
    if (!req.headers.authorization) {
        throw new Error('Unauthorized');
    }
    req.user = { id: 123, name: 'Alice' };
    return next(req);
};

const parseBody = next => req => {
    if (req.body) {
        req.parsedBody = JSON.parse(req.body);
    }
    return next(req);
};

const rateLimit = limit => next => {
    let count = 0;
    return req => {
        count++;
        if (count > limit) {
            throw new Error('Rate limit exceeded');
        }
        return next(req);
    };
};

// Final handler
const handler = req => {
    return {
        status: 200,
        body: { message: 'Success', user: req.user }
    };
};

// Compose middleware
function composeMiddleware(...middlewares) {
    return middlewares.reduceRight(
        (next, middleware) => middleware(next),
        handler
    );
}

const app = composeMiddleware(
    logger,
    auth,
    parseBody,
    rateLimit(100)
);

// Use it
const request = {
    method: 'POST',
    url: '/api/data',
    headers: { authorization: 'Bearer token' },
    body: '{"name":"test"}'
};

try {
    const response = app(request);
    console.log(response);
} catch (error) {
    console.error(error.message);
}

Practical Example: Event Processing Pipeline

// Event processors
const validateEvent = event => {
    if (!event.type || !event.data) {
        throw new Error('Invalid event format');
    }
    return event;
};

const enrichEvent = event => ({
    ...event,
    timestamp: Date.now(),
    id: Math.random().toString(36).substr(2, 9)
});

const filterByType = types => event => {
    if (!types.includes(event.type)) {
        throw new Error(`Event type ${event.type} not allowed`);
    }
    return event;
};

const logEvent = event => {
    console.log(`[${event.timestamp}] ${event.type}:`, event.data);
    return event;
};

const saveEvent = event => {
    // Simulate saving to database
    console.log('Saved event:', event.id);
    return event;
};

// Create event processor
const processEvent = pipe(
    validateEvent,
    enrichEvent,
    filterByType(['user.created', 'user.updated', 'user.deleted']),
    logEvent,
    saveEvent
);

// Use it
try {
    processEvent({
        type: 'user.created',
        data: { name: 'Alice', email: 'alice@example.com' }
    });
} catch (error) {
    console.error('Event processing failed:', error.message);
}

Benefits of Function Composition

  • Modularity: Small, focused functions
  • Reusability: Combine functions in different ways
  • Testability: Easy to test individual functions
  • Readability: Clear data flow
  • Maintainability: Easy to add/remove steps

Key Takeaways

  • Composition combines simple functions into complex ones
  • compose reads right-to-left, pipe reads left-to-right
  • ✅ Closures enable configurable composed functions
  • ✅ Works with both sync and async functions
  • ✅ Perfect for data transformation and middleware patterns

Next Steps

You've completed Section 2 on Practical Patterns! In the next section, we'll explore common use cases for closures, starting with event handlers and callbacks.