JavaScript Essentials

Currying & Partial Application

45 min Lesson 49 of 60

What is Currying?

Currying is a fundamental technique in functional programming that transforms a function with multiple arguments into a sequence of functions, each accepting a single argument. Named after mathematician Haskell Curry, currying does not call a function -- it transforms it. A curried function takes its arguments one at a time: you provide the first argument and get back a new function that waits for the second argument, and so on, until all arguments have been provided and the final result is returned. This concept is one of the most powerful patterns for building reusable, composable code in JavaScript.

To understand currying concretely, consider a simple addition function. Normally you would write add(2, 3) and get 5. When curried, you instead write add(2)(3). The first call add(2) returns a new function that remembers 2, and when that returned function is called with 3, the final result of 5 is produced. This transformation opens up a world of possibilities for creating specialized functions from general ones.

Manual Currying

The simplest way to understand currying is to write curried functions by hand. Instead of defining a function that takes all its parameters at once, you nest functions so each one accepts exactly one parameter and returns the next function in the chain.

Example: Manual Currying -- Basic Addition

// Non-curried version
function addNormal(a, b) {
    return a + b;
}
console.log(addNormal(2, 3)); // 5

// Manually curried version
function addCurried(a) {
    return function(b) {
        return a + b;
    };
}

console.log(addCurried(2)(3)); // 5

// The power: create specialized functions
const addTen = addCurried(10);
console.log(addTen(5));  // 15
console.log(addTen(20)); // 30
console.log(addTen(3));  // 13

Notice how addCurried(10) returns a brand-new function that already knows the first argument is 10. This returned function is stored in addTen and can be reused as many times as needed. The inner function has access to the outer parameter a through closure -- the mechanism by which JavaScript functions remember the variables from the scope in which they were created.

Example: Manual Currying -- Three Parameters

// A curried function with three parameters
function multiply(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        };
    };
}

console.log(multiply(2)(3)(4)); // 24

// Step by step
const double = multiply(2);       // returns function(b) {...}
const doubleThenTriple = double(3); // returns function(c) {...}
const result = doubleThenTriple(4); // 24

// Create reusable specialized functions
const tripleOf = multiply(1)(3);
console.log(tripleOf(10)); // 30
console.log(tripleOf(7));  // 21
Key Insight: Every curried function with N arguments becomes a chain of N nested functions, each taking exactly one argument. The innermost function has access to all outer arguments through closures. This is not merely a stylistic choice -- it enables powerful patterns of function composition and specialization that are impossible with standard multi-argument functions.

Currying with Arrow Functions

Arrow functions make curried code dramatically more concise. Because an arrow function with a single parameter does not need parentheses around its parameter and an arrow function with a single expression does not need curly braces or a return statement, deeply nested curried functions become elegant one-liners.

Example: Arrow Function Currying

// Manual currying with arrow functions
const add = a => b => a + b;
const multiply = a => b => c => a * b * c;
const greet = greeting => name => `${greeting}, ${name}!`;

console.log(add(5)(3));           // 8
console.log(multiply(2)(3)(4));   // 24
console.log(greet('Hello')('Alice')); // "Hello, Alice!"

// Creating specialized functions
const double = multiply(2)(1);
console.log(double(5));  // 10
console.log(double(12)); // 24

const sayHello = greet('Hello');
const sayGoodbye = greet('Goodbye');
console.log(sayHello('Bob'));    // "Hello, Bob!"
console.log(sayGoodbye('Bob')); // "Goodbye, Bob!"

Compare the arrow function version const add = a => b => a + b to the traditional version with three levels of nesting. The arrow syntax reads almost like a mathematical definition, making the curried nature of the function immediately apparent. This conciseness is why currying and arrow functions are natural companions in modern JavaScript.

Automatic Currying -- Building a Curry Utility

Writing curried functions manually works well for simple cases, but for functions with many parameters or for existing non-curried functions, you need an automatic currying utility. A curry function takes any regular function and returns a curried version of it. The key insight is that you check the function's length property (its expected number of arguments) against the number of arguments provided so far. If enough arguments have been supplied, call the original function. Otherwise, return a new function that collects more arguments.

Example: Generic Curry Utility Function

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...moreArgs) {
            return curried.apply(this, args.concat(moreArgs));
        };
    };
}

// Usage with a regular function
function volume(length, width, height) {
    return length * width * height;
}

const curriedVolume = curry(volume);

// All of these work
console.log(curriedVolume(2)(3)(4));    // 24
console.log(curriedVolume(2, 3)(4));    // 24
console.log(curriedVolume(2)(3, 4));    // 24
console.log(curriedVolume(2, 3, 4));    // 24

This curry utility is flexible -- it allows you to provide arguments one at a time, several at a time, or all at once. It checks whether enough arguments have been accumulated by comparing args.length to fn.length. The fn.length property in JavaScript returns the number of parameters defined in the function signature. When the accumulated arguments meet or exceed that count, the original function executes with all collected arguments.

Example: Arrow Function Version of Curry Utility

const curry = fn =>
    function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return (...moreArgs) => curried(...args, ...moreArgs);
    };

// Example: currying a string formatting function
const formatMessage = (level, timestamp, message) =>
    `[${level}] ${timestamp}: ${message}`;

const curriedFormat = curry(formatMessage);

const errorFormat = curriedFormat('ERROR');
const warningFormat = curriedFormat('WARNING');

const now = new Date().toISOString();
console.log(errorFormat(now)('Database connection failed'));
// "[ERROR] 2024-01-15T10:30:00.000Z: Database connection failed"

console.log(warningFormat(now)('Memory usage above 80%'));
// "[WARNING] 2024-01-15T10:30:00.000Z: Memory usage above 80%"
Pro Tip: The curry utility relies on fn.length to know how many arguments are expected. This means it does not work correctly with rest parameters (...args), default parameters, or destructured parameters, because these do not count toward fn.length. For example, ((a, b = 1) => a + b).length is 1, not 2. Always use explicit parameters without defaults when writing functions intended to be curried.

Practical Currying -- Event Handlers

Currying is particularly useful in event-driven programming. When attaching event handlers, you often need to pass additional context along with the event object. Currying lets you create handler factories that produce specialized handlers for different elements or scenarios.

Example: Curried Event Handlers

// Curried event handler factory
const handleClick = action => elementId => event => {
    event.preventDefault();
    console.log(`Action: ${action}, Element: ${elementId}`);
    console.log(`Clicked at: (${event.clientX}, ${event.clientY})`);
};

// Create specialized handlers
const handleDelete = handleClick('delete');
const handleEdit = handleClick('edit');

// Attach to specific elements
const deleteBtn = document.getElementById('delete-user-42');
const editBtn = document.getElementById('edit-user-42');

if (deleteBtn) deleteBtn.addEventListener('click', handleDelete('user-42'));
if (editBtn) editBtn.addEventListener('click', handleEdit('user-42'));

// Curried handler for input validation
const validateInput = fieldName => minLength => event => {
    const value = event.target.value;
    if (value.length < minLength) {
        console.log(`${fieldName} must be at least ${minLength} characters`);
        event.target.classList.add('invalid');
    } else {
        event.target.classList.remove('invalid');
    }
};

const validateUsername = validateInput('Username')(3);
const validatePassword = validateInput('Password')(8);
const validateBio = validateInput('Bio')(20);

Practical Currying -- API Requests

Building API clients is another area where currying shines. You can create a curried request function that first accepts configuration (base URL, headers), then the endpoint, then the specific request data. Each level of currying produces a more specialized function.

Example: Curried API Client

// Curried API request builder
const createApiClient = baseUrl => defaultHeaders => method => endpoint => async data => {
    const config = {
        method,
        headers: {
            'Content-Type': 'application/json',
            ...defaultHeaders,
        },
    };

    if (data && method !== 'GET') {
        config.body = JSON.stringify(data);
    }

    const response = await fetch(`${baseUrl}${endpoint}`, config);
    return response.json();
};

// Build specialized API functions
const api = createApiClient('https://api.example.com');
const authApi = api({ 'Authorization': 'Bearer token123' });
const publicApi = api({});

// Create method-specific functions
const authGet = authApi('GET');
const authPost = authApi('POST');
const authPut = authApi('PUT');
const authDelete = authApi('DELETE');

// Create endpoint-specific functions
const getUsers = authGet('/users');
const getUser = id => authGet(`/users/${id}`);
const createUser = authPost('/users');
const updateUser = id => authPut(`/users/${id}`);

// Usage -- clean and expressive
const users = await getUsers();
const user = await getUser(42)();
const newUser = await createUser({ name: 'Alice', email: 'alice@example.com' });
const updated = await updateUser(42)({ name: 'Alice Updated' });
Key Insight: Notice how each level of currying adds specificity. The base client knows the URL. Adding headers creates an authenticated or public client. Adding the method creates a GET, POST, PUT, or DELETE client. Adding the endpoint creates a resource-specific function. Each layer is independently reusable. You build the client once and use the specialized versions throughout your application.

Practical Currying -- Validation

Form validation is a domain where currying produces elegant, declarative code. Instead of writing repetitive validation functions, you create generic validators and curry them into specific rules that can be composed together.

Example: Curried Validators

// Generic curried validators
const minLength = min => value =>
    value.length >= min ? null : `Must be at least ${min} characters`;

const maxLength = max => value =>
    value.length <= max ? null : `Must be at most ${max} characters`;

const matches = regex => message => value =>
    regex.test(value) ? null : message;

const required = fieldName => value =>
    value && value.trim().length > 0 ? null : `${fieldName} is required`;

const isEmail = matches(
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/
)('Please enter a valid email address');

const isNumeric = matches(
    /^\d+$/
)('Must contain only numbers');

// Compose validators into field-specific rules
const validateField = validators => value => {
    for (const validate of validators) {
        const error = validate(value);
        if (error) return error;
    }
    return null;
};

// Define field rules using curried validators
const usernameRules = validateField([
    required('Username'),
    minLength(3),
    maxLength(20),
    matches(/^[a-zA-Z0-9_]+$/)('Username can only contain letters, numbers, and underscores'),
]);

const emailRules = validateField([
    required('Email'),
    isEmail,
]);

const passwordRules = validateField([
    required('Password'),
    minLength(8),
    matches(/[A-Z]/)('Must contain at least one uppercase letter'),
    matches(/[0-9]/)('Must contain at least one number'),
]);

// Usage
console.log(usernameRules('ab'));           // "Must be at least 3 characters"
console.log(usernameRules('valid_user'));    // null (valid)
console.log(emailRules('not-an-email'));     // "Please enter a valid email address"
console.log(passwordRules('weak'));          // "Must be at least 8 characters"

Partial Application vs Currying

Currying and partial application are related but distinct concepts, and confusing them is one of the most common mistakes in functional programming discussions. Currying transforms a function of N arguments into N functions of one argument each. Partial application fixes one or more arguments of a function and returns a new function that takes the remaining arguments -- but that remaining function can still accept multiple arguments at once.

Example: Currying vs Partial Application

// Original function
function add(a, b, c) {
    return a + b + c;
}

// CURRYING: transforms into a chain of unary functions
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3); // 6
// Each call takes exactly ONE argument

// PARTIAL APPLICATION: fixes some arguments, returns function for the rest
function partialAdd(a) {
    return function(b, c) {  // accepts MULTIPLE remaining arguments
        return a + b + c;
    };
}
partialAdd(1)(2, 3); // 6
// First call fixes one argument, second call provides ALL remaining

// A generic partial application utility
function partial(fn, ...fixedArgs) {
    return function(...remainingArgs) {
        return fn(...fixedArgs, ...remainingArgs);
    };
}

const addOne = partial(add, 1);
console.log(addOne(2, 3)); // 6  -- provides both remaining args at once

const addOneTwo = partial(add, 1, 2);
console.log(addOneTwo(3)); // 6  -- only one remaining arg

The critical distinction is this: a curried function always returns a unary function (one argument) until all arguments are provided. A partially applied function can return a function that still accepts multiple arguments. Currying is a specific transformation of function structure; partial application is a more general technique of fixing arguments. In practice, the automatic curry utility we built earlier blurs this line because it allows providing multiple arguments at once, making it behave like both currying and partial application.

Function.prototype.bind for Partial Application

JavaScript has a built-in mechanism for partial application: the bind method. While bind is most commonly used to fix the this context of a function, it also accepts additional arguments that are prepended to the argument list when the bound function is called.

Example: Partial Application with bind

function multiply(a, b) {
    return a * b;
}

// Use bind to create partially applied functions
// null for this context since we don't need it
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
const tenTimes = multiply.bind(null, 10);

console.log(double(5));   // 10
console.log(triple(5));   // 15
console.log(tenTimes(5)); // 50

// Works with more arguments
function createTag(tag, className, content) {
    return `<${tag} class="${className}">${content}</${tag}>`;
}

const createDiv = createTag.bind(null, 'div');
const createHighlightDiv = createTag.bind(null, 'div', 'highlight');
const createParagraph = createTag.bind(null, 'p');

console.log(createDiv('container', 'Hello World'));
// "<div class=\"container\">Hello World</div>"

console.log(createHighlightDiv('Important text'));
// "<div class=\"highlight\">Important text</div>"

console.log(createParagraph('lead', 'First paragraph'));
// "<p class=\"lead\">First paragraph</p>"
Important: When using bind for partial application, the first argument to bind sets the this context. If you are working with a standalone function (not a method), pass null as the first argument. However, if you are partially applying a method on an object, pass the object as the first argument to preserve the correct this binding. Mixing up the this context is a common source of bugs when using bind for partial application.

Placeholder Partial Application

Standard partial application and bind fix arguments from left to right. But sometimes you need to fix arguments in a different order. Placeholder partial application lets you skip positions using a sentinel value (a placeholder) and fill them in later. This is especially useful when the argument you want to fix is not the first one.

Example: Partial Application with Placeholders

// Placeholder symbol
const _ = Symbol('placeholder');

function partialWithPlaceholders(fn, ...partialArgs) {
    return function(...laterArgs) {
        const args = [];
        let laterIndex = 0;

        // Fill in placeholders with later arguments
        for (const arg of partialArgs) {
            if (arg === _) {
                args.push(laterArgs[laterIndex++]);
            } else {
                args.push(arg);
            }
        }

        // Append any remaining later arguments
        while (laterIndex < laterArgs.length) {
            args.push(laterArgs[laterIndex++]);
        }

        return fn(...args);
    };
}

// Example: fixing the second argument
function divide(a, b) {
    return a / b;
}

const divideBy2 = partialWithPlaceholders(divide, _, 2);
console.log(divideBy2(10)); // 5
console.log(divideBy2(20)); // 10

const half = partialWithPlaceholders(divide, _, 2);
const divideFrom100 = partialWithPlaceholders(divide, 100, _);

console.log(half(50));        // 25
console.log(divideFrom100(4)); // 25

// Useful for Array methods
const map = partialWithPlaceholders(Array.prototype.map.call.bind(Array.prototype.map), _, _);
const replace = partialWithPlaceholders(
    (str, search, replacement) => str.replace(search, replacement),
    _,
    /[aeiou]/gi,
    '*'
);

console.log(replace('Hello World')); // "H*ll* W*rld"

Point-Free Style

Point-free style (also called tacit programming) is a way of writing functions without explicitly mentioning their arguments. Instead of defining what happens to the data, you compose operations together. Currying is essential for point-free style because it produces functions that are ready to accept data as their final argument.

Example: Point-Free Style with Curried Functions

// Curried utility functions
const map = fn => arr => arr.map(fn);
const filter = predicate => arr => arr.filter(predicate);
const reduce = (fn, initial) => arr => arr.reduce(fn, initial);
const prop = key => obj => obj[key];
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

// Point-free: no explicit mention of data
const getNames = map(prop('name'));
const getActiveUsers = filter(prop('active'));
const sum = reduce((a, b) => a + b, 0);
const toUpperCase = str => str.toUpperCase();
const upperNames = map(toUpperCase);

// Compose into pipelines
const getActiveNames = pipe(
    getActiveUsers,
    getNames,
    upperNames
);

const users = [
    { name: 'Alice', active: true },
    { name: 'Bob', active: false },
    { name: 'Charlie', active: true },
    { name: 'Diana', active: true },
];

console.log(getActiveNames(users));
// ["ALICE", "CHARLIE", "DIANA"]

// Compare to imperative version
const imperativeResult = users
    .filter(u => u.active)
    .map(u => u.name)
    .map(n => n.toUpperCase());
// Same result but explicitly mentions data at every step
Pro Tip: Point-free style works best when your curried functions follow the convention of accepting the data argument last. This is sometimes called "data-last" design. Utility libraries like Ramda follow this convention throughout, making every function naturally composable in a point-free style. If you design your own curried functions, always put the data (the thing being transformed) as the final argument, and put configuration or behavior arguments first.

Currying for Configuration

One of the most practical applications of currying is building configurable functions. The configuration arguments come first, and the data to process comes last. This creates a clean separation between setup and execution, and the configured function can be stored and reused across your application.

Example: Curried Configuration Patterns

// Configurable logger
const createLogger = level => prefix => message => {
    const timestamp = new Date().toISOString();
    const formatted = `[${timestamp}] [${level}] [${prefix}] ${message}`;

    if (level === 'ERROR') {
        console.error(formatted);
    } else if (level === 'WARNING') {
        console.warn(formatted);
    } else {
        console.log(formatted);
    }

    return formatted;
};

// Create specialized loggers
const errorLog = createLogger('ERROR');
const warnLog = createLogger('WARNING');
const infoLog = createLogger('INFO');

// Further specialize by module
const dbError = errorLog('Database');
const dbInfo = infoLog('Database');
const authError = errorLog('Auth');
const authInfo = infoLog('Auth');
const apiInfo = infoLog('API');

// Usage throughout the application
dbError('Connection timeout after 30s');
dbInfo('Query executed in 45ms');
authError('Invalid token for user 42');
authInfo('User 42 logged in successfully');
apiInfo('GET /users responded with 200');

// Configurable string formatter
const format = template => separator => values =>
    template.replace(/\{\}/g, () => values.shift() || separator);

const csvLine = format('{},{},{}')('');
const tsvLine = format('{}\t{}\t{}')('');

console.log(csvLine(['Alice', '30', 'Engineer']));
// "Alice,30,Engineer"

Real-World Example: Curried Form Validators

Let us build a complete, production-quality form validation system using currying. Each validator is a curried function that first accepts its configuration (error messages, constraints) and then accepts the value to validate. Validators return either null for valid input or an error string for invalid input.

Example: Complete Curried Validation System

// Base validators -- each is curried for configuration
const validators = {
    required: message => value =>
        value !== null && value !== undefined && String(value).trim() !== ''
            ? null
            : message || 'This field is required',

    minLength: (min, message) => value =>
        String(value).length >= min
            ? null
            : message || `Minimum ${min} characters required`,

    maxLength: (max, message) => value =>
        String(value).length <= max
            ? null
            : message || `Maximum ${max} characters allowed`,

    pattern: (regex, message) => value =>
        regex.test(String(value))
            ? null
            : message || 'Invalid format',

    range: (min, max, message) => value => {
        const num = Number(value);
        return num >= min && num <= max
            ? null
            : message || `Must be between ${min} and ${max}`;
    },

    custom: (testFn, message) => value =>
        testFn(value) ? null : message,
};

// Compose multiple validators for a single field
const composeValidators = (...rules) => value => {
    for (const rule of rules) {
        const error = rule(value);
        if (error) return error;
    }
    return null;
};

// Define a complete form schema
const formSchema = {
    username: composeValidators(
        validators.required('Username is required'),
        validators.minLength(3, 'Username must be at least 3 characters'),
        validators.maxLength(20, 'Username cannot exceed 20 characters'),
        validators.pattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores')
    ),
    email: composeValidators(
        validators.required('Email is required'),
        validators.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Invalid email format')
    ),
    age: composeValidators(
        validators.required('Age is required'),
        validators.range(18, 120, 'Must be between 18 and 120')
    ),
    password: composeValidators(
        validators.required('Password is required'),
        validators.minLength(8, 'Password must be at least 8 characters'),
        validators.pattern(/[A-Z]/, 'Must contain an uppercase letter'),
        validators.pattern(/[a-z]/, 'Must contain a lowercase letter'),
        validators.pattern(/[0-9]/, 'Must contain a number')
    ),
};

// Validate the entire form
function validateForm(schema, data) {
    const errors = {};
    for (const [field, validate] of Object.entries(schema)) {
        const error = validate(data[field] || '');
        if (error) errors[field] = error;
    }
    return Object.keys(errors).length > 0 ? errors : null;
}

// Test it
const formData = {
    username: 'ab',
    email: 'invalid',
    age: '15',
    password: 'weak',
};

console.log(validateForm(formSchema, formData));
// {
//   username: "Username must be at least 3 characters",
//   email: "Invalid email format",
//   age: "Must be between 18 and 120",
//   password: "Password must be at least 8 characters"
// }

Real-World Example: Curried API Client with Error Handling

Here is a robust API client that uses currying to create a layered, configurable HTTP client. Each layer adds specificity: base configuration, authentication, HTTP method, endpoint, and finally the request body. Error handling and response parsing are baked into the curried layers.

Example: Production API Client with Currying

// Curried API client builder with error handling
const createClient = baseConfig => authHeaders => method => endpoint => async (body = null) => {
    const url = `${baseConfig.baseUrl}${endpoint}`;

    const options = {
        method,
        headers: {
            'Content-Type': 'application/json',
            ...baseConfig.defaultHeaders,
            ...authHeaders,
        },
    };

    if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
        options.body = JSON.stringify(body);
    }

    try {
        const response = await fetch(url, options);

        if (!response.ok) {
            const errorData = await response.json().catch(() => ({}));
            throw {
                status: response.status,
                statusText: response.statusText,
                data: errorData,
                url,
            };
        }

        const contentType = response.headers.get('content-type');
        if (contentType && contentType.includes('application/json')) {
            return await response.json();
        }
        return await response.text();
    } catch (error) {
        if (error.status) throw error;
        throw { status: 0, statusText: 'Network Error', data: error.message, url };
    }
};

// Configure the client in layers
const config = { baseUrl: 'https://api.example.com/v2', defaultHeaders: {} };
const apiClient = createClient(config);

// Public client (no auth)
const publicApi = apiClient({});
const publicGet = publicApi('GET');

// Authenticated client
const authToken = 'eyJhbGciOiJIUzI1NiJ9...';
const authedApi = apiClient({ 'Authorization': `Bearer ${authToken}` });
const get = authedApi('GET');
const post = authedApi('POST');
const put = authedApi('PUT');
const del = authedApi('DELETE');

// Resource-specific functions
const getUsers = get('/users');
const getUserById = id => get(`/users/${id}`);
const createUser = post('/users');
const updateUser = id => put(`/users/${id}`);
const deleteUser = id => del(`/users/${id}`);

// Usage
const allUsers = await getUsers();
const user = await getUserById(42)();
const newUser = await createUser({ name: 'Alice', role: 'admin' });

When to Use Currying and When to Avoid It

Currying is powerful, but it is not the right tool for every situation. Use currying when you need to create families of related functions from a general template, when building configurable utilities, when working with function composition and pipelines, when designing event handler factories, or when implementing point-free style. Avoid currying when the function has a variable number of arguments, when all arguments are always available at the same call site, when it reduces readability for your team, or when performance is critical in tight loops (each curried call creates a new function object and closure).

Important: Overusing currying can make code harder to read, especially for developers unfamiliar with functional programming patterns. The goal is clarity, not cleverness. Use currying when it genuinely simplifies your code by eliminating repetition or enabling composition. If a curried version is harder to understand than a plain function call, prefer the simpler approach. Always consider your team's familiarity with these patterns before adopting them widely in a shared codebase.

Practice Exercise

Build a complete data transformation pipeline using currying and function composition. Start by implementing a curry utility function that can transform any regular function into a curried version. Then create the following curried utility functions: map that takes a transform function then an array, filter that takes a predicate then an array, sort that takes a comparator then an array, take that takes a count then an array, and prop that takes a key name then an object. Next, implement a pipe function that composes functions left to right. Using these utilities, build a data processing pipeline that takes an array of product objects (each with name, price, category, and inStock properties) and performs the following transformations in a single composed pipeline: filter to only in-stock products, filter to products in a specific category (make this configurable via currying), sort by price ascending, take only the first 5 results, and extract just the names. The final pipeline should be a single function that accepts the category as its first argument and the product array as its second argument. Test your pipeline with at least 10 sample products across 3 categories, and verify that changing the category argument produces different results without modifying the pipeline code.