JavaScript and TypeScript course Β· Module 12: Functional Programming

Currying and Partial Application

6 min read
In this lesson5

Imagine the dinosaur feeding system in the park. Each species has its own requirements - type of food, portion size, and feeding time. Instead of providing all parameters every time, we can prepare "pre-configured" functions for a given species. This is exactly what currying does - it transforms a multi-argument function into a sequence of single-argument functions.

There is also a second, more technical problem. pipe from the previous lesson passes exactly one value to each stage, so a function that needs a species, a food and a portion simply does not fit into it. We need a way to set some of the arguments in advance and end up with a single-argument function.

What is Currying?

Currying is a technique of transforming a function that takes multiple arguments into a sequence of functions, each taking exactly one argument.

Let's start with a regular function with three parameters and its curried version written the classic way, with nested function expressions:

1// Regular function with 3 arguments
2function feedDinosaur(species, food, portion) {
3  return species + ' gets ' + portion + 'kg of ' + food;
4}
5feedDinosaur('T-Rex', 'meat', 50);
6
7// Curried version - sequence of single-argument functions
8function feedDinosaurCurried(species) {
9  return function(food) {
10    return function(portion) {
11      return species + ' gets ' + portion + 'kg of ' + food;
12    };
13  };
14}
15feedDinosaurCurried('T-Rex')('meat')(50);

Both calls return 'T-Rex gets 50kg of meat'. The call feedDinosaurCurried('T-Rex') does not feed anyone yet, it only returns a function waiting for the food, and only the third pair of parentheses runs the actual logic. The inner functions remember species and food thanks to closures, which we will look at in the next lesson.

Arrow functions let you write the same thing much more briefly, because each arrow is another stage:

1// Same thing with arrow functions (more concise)
2const feedCurried = (species) => (food) => (portion) =>
3  species + ' gets ' + portion + 'kg of ' + food;

Read it from the left: the function of the species returns a function of the food, which in turn returns a function of the portion. Only the notation has changed, the behavior is identical to the version with function.

Practical Use of Currying

The real power of currying lies in creating specialized functions. This time the last stage returns a feeding object with a timestamp:

1const feedCurried = (species) => (food) => (portion) =>
2  ({ species, food, portion, timestamp: Date.now() });
3
4// Specialized functions for species
5const feedRex = feedCurried('T-Rex');
6const feedRexMeat = feedRex('meat');
7
8// Using specialized functions
9feedRexMeat(50);  // { species: 'T-Rex', food: 'meat', portion: 50, ... }
10feedRexMeat(30);  // { species: 'T-Rex', food: 'meat', portion: 30, ... }
11
12// Can also be used for another species
13const feedBrachio = feedCurried('Brachiosaurus');
14const feedBrachioPlants = feedBrachio('plants');
15feedBrachioPlants(200); // { species: 'Brachiosaurus', food: 'plants', portion: 200, ... }

feedRex and feedRexMeat are ordinary functions that you can store, pass around and call many times, while feedCurried stays general and handles the Brachiosaurus without any trouble. The notation { species, food, portion } is shorthand for { species: species, ... }. A technical note: Date.now() makes the last stage impure, because two calls with the same portion give different timestamps. The version without the time, const feed = (species) => (food) => (portion) => ({ species, food, portion }), is pure.

Universal Curry Function

We can write a universal curry function that automatically transforms any function. The key is the fn.length property, which is the number of parameters the function declares (excluding the rest parameter and any parameters from the first one with a default value onwards):

1function curry(fn) {
2  return function curried(...args) {
3    if (args.length >= fn.length) {
4      return fn(...args);
5    }
6    return (...moreArgs) => curried(...args, ...moreArgs);
7  };
8}

curried collects arguments. Once it has at least as many as fn.length, it calls the original, and otherwise it returns a function that will add more. Let's check it on a function that builds an enclosure:

1// Usage
2function createEnclosure(zone, type, capacity, voltage) {
3  return { zone, type, capacity, voltage };
4}
5
6const curriedCreate = curry(createEnclosure);
7
8// Can be called in various ways:
9curriedCreate('A')('carnivore')(5)(10000);
10curriedCreate('A', 'carnivore')(5, 10000);
11curriedCreate('A', 'carnivore', 5)(10000);
12curriedCreate('A', 'carnivore', 5, 10000);
13// All calls produce the same result

Every call returns { zone: 'A', type: 'carnivore', capacity: 5, voltage: 10000 }. Strictly speaking, a version that accepts several arguments at once combines currying with partial application. That is also how the curry functions in the Ramda and lodash libraries work. A trap: a function declared as (...args) => ... has a length of 0, so curry will call it immediately.

Partial Application

Partial application is a related technique - it "freezes" some of a function's arguments, creating a new function with fewer parameters.

partial takes a function and any number of starting arguments (presetArgs), and returns a function waiting for the rest (laterArgs). The spread operator glues both lists into a single call:

1function partial(fn, ...presetArgs) {
2  return (...laterArgs) => fn(...presetArgs, ...laterArgs);
3}

Let's apply it to the incident logging function used by the whole park security team, and build a few specialized loggers out of it:

1// Incident logging function
2function logIncident(severity, zone, message) {
3  return '[' + severity + '] Zone ' + zone + ': ' + message;
4}
5
6// Specialized loggers
7const logCritical = partial(logIncident, 'CRITICAL');
8const logWarning = partial(logIncident, 'WARNING');
9const logCriticalZoneA = partial(logIncident, 'CRITICAL', 'A');
10
11logCritical('B', 'Fence breach detected');
12// '[CRITICAL] Zone B: Fence breach detected'
13
14logCriticalZoneA('T-Rex escaped!');
15// '[CRITICAL] Zone A: T-Rex escaped!'

In logCritical one argument is frozen, and in logCriticalZoneA two at once. The original logIncident has not changed and still takes three arguments. The built-in bind gives the same effect: logIncident.bind(null, 'CRITICAL').

Currying vs Partial Application

Although both techniques create specialized functions, they differ in how they work:

  • Currying: always creates a chain of single-argument functions
  • Partial application: "freezes" any number of arguments at once

Specializing through currying always has the same stages: you define a general curried function, you pass the first argument and get a partially applied function, further arguments narrow it down, and the last one returns the result. My advice: write functions for pipelines in the order "configuration first, data last", for example filterBy(key)(value)(arr). Once the configuration is given, what remains is a single-argument function ready for pipe, and for a one-off freezing of a few arguments of an existing function, partial is enough. In the final project you will build the tools for filtering readings exactly this way.

Remember: currying is a feeding station configured step by step - species, food, portion - and every setting gives you a new machine that is ready to work.

Code for this lesson: index.js
1// Currying and partial application
2console.log("=== Jurassic Park - Staged Species Processing ===\n");
3
4// --- EXAMPLE 1: Basic currying ---
5console.log("--- Currying: Feeding system ---");
6
7const feedDinosaur = (species) => (food) => (portion) =>
8  `${species} gets ${portion}kg of ${food}`;
9
10// Specialized functions
11const feedRex = feedDinosaur("T-Rex");
12const feedRexMeat = feedRex("meat");
13
14console.log(feedRexMeat(50));  // T-Rex gets 50kg of meat
15console.log(feedRexMeat(30));  // T-Rex gets 30kg of meat
16
17const feedBrachio = feedDinosaur("Brachiosaurus");
18console.log(feedBrachio("plants")(200)); // Brachiosaurus gets 200kg of plants
19
20// --- EXAMPLE 2: A universal curry function ---
21console.log("\n--- Universal curry ---");
22
23function curry(fn) {
24  return function curried(...args) {
25    if (args.length >= fn.length) {
26      return fn(...args);
27    }
28    return (...moreArgs) => curried(...args, ...moreArgs);
29  };
30}
31
32function createEnclosure(zone, type, capacity, voltage) {
33  return { zone, type, capacity, voltage };
34}
35
36const curriedCreate = curry(createEnclosure);
37
38// Different ways of calling it
39const r1 = curriedCreate("A")("carnivore")(5)(10000);
40const r2 = curriedCreate("A", "carnivore")(5, 10000);
41const r3 = curriedCreate("A", "carnivore", 5, 10000);
42
43console.log("Call 1:", JSON.stringify(r1));
44console.log("Call 2:", JSON.stringify(r2));
45console.log("Identical?", JSON.stringify(r1) === JSON.stringify(r2));
46
47// Specialized factories
48const createCarnivoreZone = curriedCreate("X")("carnivore");
49console.log("Zone X-5:", JSON.stringify(createCarnivoreZone(5)(15000)));
50console.log("Zone X-10:", JSON.stringify(createCarnivoreZone(10)(20000)));
51
52// --- EXAMPLE 3: Partial application ---
53console.log("\n--- Partial application ---");
54
55function partial(fn, ...presetArgs) {
56  return (...laterArgs) => fn(...presetArgs, ...laterArgs);
57}
58
59function logIncident(severity, zone, message) {
60  return `[${severity}] Zone ${zone}: ${message}`;
61}
62
63const logCritical = partial(logIncident, "CRITICAL");
64const logWarning = partial(logIncident, "WARNING");
65const logCriticalA = partial(logIncident, "CRITICAL", "A");
66
67console.log(logCritical("B", "Fence breach"));
68console.log(logWarning("A", "Sensor malfunction"));
69console.log(logCriticalA("T-Rex escaped!"));
70
71// --- EXAMPLE 4: Currying with map/filter ---
72console.log("\n--- Currying in practice ---");
73
74const filterByField = curry((field, value, arr) =>
75  arr.filter((item) => item[field] === value)
76);
77
78const dinosaurs = [
79  { name: "Rex", diet: "carnivore", zone: "A" },
80  { name: "Blue", diet: "carnivore", zone: "A" },
81  { name: "Brachio", diet: "herbivore", zone: "B" },
82  { name: "Stego", diet: "herbivore", zone: "B" },
83];
84
85const filterByDiet = filterByField("diet");
86const getCarnivores = filterByDiet("carnivore");
87const getHerbivores = filterByDiet("herbivore");
88
89console.log("Predators:", getCarnivores(dinosaurs).map((d) => d.name));
90console.log("Herbivores:", getHerbivores(dinosaurs).map((d) => d.name));

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is currying in functional programming?

  2. 2. How does partial application differ from currying?

Hands-on tasks in the game

  • Code editor

    Use a universal curry function to create specialized filters and loggers.

  • Vertical ordering

    Arrange the stages of creating a specialized function through currying:

  • Click in order

    Arrange the elements of a curried arrow function:

  • Code editor

    Use partial() to create specialized filters and combine them into a pipeline with pipe().

Useful articles