JS Pro Tips & Patterns - Part 2
Currying, Higher Order Functions, Closures, Generators & Other Functional Features in JavaScript
Currying
Currying is a functional programming technique where a function with multiple arguments is transformed into a series of nested functions, each taking a single argument. Instead of calling a function with all its arguments at once (f(a, b, c)), you call a sequence of functions, each with one argument (f(a)(b)(c)).
How Currying Works
Suppose you have a function that adds two numbers:
function add(a, b) {
return a + b;
}
console.log(add(2, 4)); // 6
With currying, you rewrite it so that it takes one argument at a time:
function add(a) {
return function(b) {
return a + b;
}
}
const add5 = add(5); // Returns a function that adds 5 to its argument
console.log(add5(3)); // 8
Or using ES6 arrow functions:
const add = a => b => a + b;
console.log(add(2)(4)); // 6
Currying always returns a function until all arguments are supplied. All curried functions are higher order functions.
Currying with More Arguments
You can curry functions with more than two arguments:
- Curried
- ES6(Curried)
- Standard Function (Not Curried)
function sum(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
console.log(sum(1)(2)(3)); // 6
const sum = a => b => c => a + b + c;
console.log(sum(1)(2)(3)); // 6
function sum(a, b, c) {
return a + b + c;
}
console.log(sum(1, 2, 3)); // 6
Usage and Benefits
- Partial Application: Currying allows you to create specialized functions by pre-filling some arguments (by supplying some arguments in advance).
- 3 Arguments
- 2 Arguments
const sum = a => b => c => a + b + c;
const add10And = sum(10); // returns a function expecting b
const add10And5 = add10And(5); // returns a function expecting c
console.log(add10And5(2)); // 17 (10 + 5 + 2)
const multiply = a => b => a * b;
const double = multiply(2);
console.log(double(10)); // 20
- Function Composition: Currying makes it easier to compose functions and build complex operations from simple ones.
- Reusability: You can reuse curried functions in different contexts by supplying different arguments.
Real-World Example
Suppose you want to filter a list of users by role:
const hasRole = role => user => user.role === role;
const isAdmin = hasRole('admin');
const users = [{name: 'A', role: 'admin'}, {name: 'B', role: 'user'}];
const admins = users.filter(isAdmin); // [{name: 'A', role: 'admin'}]
Higher Order Functions
A higher order function is any function that does at least one of the following:
- Takes one or more functions as arguments
- Returns a function as its result
All curried functions are higher order functions (because they return functions), but not all higher order functions are curried. Currying is a specific use-case of higher order functions focused on argument handling.
Common Examples in JavaScript
-
Array methods:
map,filter,reduce,forEach, etc.const nums = [1, 2, 3, 4];
const squared = nums.map(x => x * x); // [1, 4, 9, 16]
const evens = nums.filter(x => x % 2 === 0); // [2, 4] -
Custom Higher Order Function:
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log); // Logs 0, 1, 2
Practical Example: withLogging
function withLogging(fn) {
return function(...args) {
console.log('Calling function with args:', args);
return fn(...args);
}
}
const sum = (a, b) => a + b;
const loggedSum = withLogging(sum);
console.log(loggedSum(2, 3)); // Logs args and returns 5
Explanation:
withLoggingis a higher order function because it takes a function (fn) as an argument and returns a new function.- When you call
withLogging(sum), it returns a new function (loggedSum) that:- Logs the arguments it receives.
- Calls the original
sumfunction with those arguments.
- So,
loggedSum(2, 3)logsCalling function with args:[2, 3]and returnssum(2, 3)which is5. loggedSumis a function that takes numbers as arguments, not a function that takes a function.
Another Practical Example: Logger
function logger(formatFn) {
return function(message) {
console.log(formatFn(message));
}
}
const upperLogger = logger(msg => msg.toUpperCase());
upperLogger('hello'); // "HELLO"
- Here,
loggeris a higher order function because it takes a function as an argument and returns a new function. - This is not currying, because it's not about splitting arguments into multiple function calls; it's about customizing behavior by passing a function.
Reusability with Higher Order Functions
Higher order functions enable reusability by allowing you to abstract common logic and inject custom behavior:
function withLogging(fn) {
return function(...args) {
console.log('Calling with:', args);
return fn(...args);
}
}
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const loggedAdd = withLogging(add);
const loggedMultiply = withLogging(multiply);
console.log(loggedAdd(2, 3)); // Logs and returns 5
console.log(loggedMultiply(2, 3)); // Logs and returns 6
You can wrap any function with withLogging to add logging, without changing the original function.
Closures
A closure is a feature in JavaScript where an inner function has access to variables from its outer (enclosing) function scope, even after the outer function has finished executing.
Example
function outer() {
let count = 0;
return function() {
count++;
return count;
}
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
Explanation:
- The inner function returned by
outer"remembers" thecountvariable, even afterouterhas finished running. - This is possible because of closures.
Closures in Currying and Higher Order Functions
- Currying relies on closures to "remember" the arguments passed in previous function calls.
- Higher order functions often use closures to maintain state or configuration.
Example: Currying with Closure
const add = a => b => a + b;
// The inner function "remembers" the value of `a` from the outer function.
All curried functions are higher order functions and use closures, but not all higher order functions are curried, and not all closures are higher order functions.
Generator Functions (function*)
Generator functions are special functions in JavaScript that can be paused and resumed, allowing you to produce a sequence of values over time, instead of returning a single value.
How to Define a Generator Function
- Use the
function*syntax (note the asterisk). - Inside the function, use the
yieldkeyword to produce values one at a time.
Example:
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
How Generators Work
- Each call to
gen.next()resumes the function until the nextyield. - The function's state is saved between calls, so you can produce values on demand.
- When there are no more
yieldstatements,donebecomestrue.
Iterating Over Generators
Generators are iterable, so you can use them in loops:
function* colors() {
yield 'red';
yield 'green';
yield 'blue';
}
for (const color of colors()) {
console.log(color); // red, green, blue
}
Use Cases for Generators
- Lazy evaluation: Generate values only when needed (e.g., large data sets, infinite sequences).
- Custom iterators: Implement your own iteration logic for objects.
- Asynchronous control flow: With
asyncgenerators andfor await...of, you can handle streams of asynchronous data. - State machines: Pause and resume execution at specific points.
Example: Infinite Sequence
function* infiniteNumbers() {
let n = 1;
while (true) {
yield n++;
}
}
const gen = infiniteNumbers();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
// ...and so on
Example: Custom Iterable Object
const range = {
from: 1,
to: 5,
[Symbol.iterator]: function* () {
for (let i = this.from; i <= this.to; i++) {
yield i;
}
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
Other Special Functional Features in JavaScript
Pure Functions
A pure function is a function that, given the same input, will always return the same output and does not cause any side effects (like modifying external variables, logging, or changing the DOM).
Characteristics:
- No side effects (does not modify anything outside itself)
- Output depends only on input
Example:
function add(a, b) {
return a + b; // Pure: always same output for same input, no side effects
}
let total = 0;
function impureAdd(a) {
total += a; // Impure: modifies external variable
return total;
}
Why use pure functions?
- Easier to test and debug
- More predictable and reliable
- Enable functional programming patterns like memoization and composition
Pure functions and first-class functions are different concepts:
- A pure function is about how a function behaves (no side effects, same output for same input).
- First-class functions is about how functions are treated by the language (can be assigned, passed, returned, etc.). A function can be both pure and first-class, but the concepts are not the same.
First-Class Functions & First-Class Citizens
A first-class citizen in a programming language refers to an entity that can be treated like any other value or variable in that language.
In JavaScript, first-class functions are functions that are treated as values, just like numbers or strings. This means:
- Functions can be assigned to variables
- Passed as arguments to other functions
- Returned from other functions
- Stored in data structures (like arrays or objects)
This property is often called "first-class citizens" or "first-class functions."
When we say "JavaScript has first-class functions," we mean that functions are first-class citizens in JavaScript.
Example:
function greet(name) {
return `Hello, ${name}!`;
}
const sayHello = greet; // Assign function to variable
function processUser(fn, userName) {
return fn(userName); // Pass function as argument
}
console.log(processUser(sayHello, 'Alice')); // "Hello, Alice!"
function multiplier(factor) {
return function(x) {
return x * factor;
}
}
const double = multiplier(2); // Return function from function
console.log(double(5)); // 10
Why is this important?
- Enables higher order functions, callbacks, currying, and many advanced patterns
- Makes JavaScript a flexible and expressive language
Immutability
Immutability means not modifying data directly, but instead creating new copies with the desired changes.
This helps prevent bugs and makes code more predictable.
Example:
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // [1, 2, 3, 4]
Recursion
Recursion is when a function calls itself to solve a problem by breaking it down into smaller subproblems.
Example:
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
Composition
Function composition is combining two or more functions to produce a new function.
Example:
const double = x => x * 2;
const square = x => x * x;
const doubleThenSquare = x => square(double(x));
console.log(doubleThenSquare(3)); // 36
Anonymous Functions (Lambdas)
Anonymous functions are functions without a name, often used as arguments to higher order functions.
In JavaScript, "lambdas" primarily refer to arrow functions.
Arrow functions provide a concise syntax for writing anonymous functions and are commonly used as callbacks.
Example:
[1, 2, 3].map(function(x) { return x * 2; });
// or with arrow syntax (lambda):
[1, 2, 3].map(x => x * 2);
Callback Functions
A callback function is a function passed as an argument to another function, to be executed later.
Example:
setTimeout(() => {
console.log('Executed after 1 second');
}, 1000);
Memoization
Memoization is an optimization technique to cache the results of expensive function calls.
Example:
function memoize(fn) {
const cache = {};
return function(x) {
if (cache[x]) return cache[x];
cache[x] = fn(x);
return cache[x];
}
}
const square = memoize(x => x * x);
console.log(square(4)); // 16 (computed)
console.log(square(4)); // 16 (cached)
Key Differences & Summary
- Currying is a specific pattern of higher order functions where each function takes only one argument and returns another function (until all arguments are provided). Currying uses closures to remember previous arguments.
- Higher order functions is a broader term for any function that takes or returns another function.
- Closure is a JavaScript feature that allows functions to remember their lexical scope, enabling currying and many higher order function patterns.
- Generator functions (
function*) allow you to pause and resume execution, producing sequences of values withyield. - Pure functions always return the same output for the same input and have no side effects.
- First-class functions means functions can be treated like any other value in JavaScript. "First-class citizens" and "first-class functions" mean the same thing in this context.
- Immutability, recursion, composition, anonymous functions (lambdas), callbacks, and memoization are also important functional programming concepts in JavaScript.
Summary Table
| Feature/Concept | Returns Function? | Uses Function as Arg? | Uses Closure? | Side Effects? | Example Pattern |
|---|---|---|---|---|---|
| Higher Order Function | Sometimes | Sometimes | Sometimes | Maybe | g(fn) or g()() |
| Currying | Yes | No | Yes | No | f(a)(b)(c) |
| Closure | Sometimes | No | Yes | Maybe | function outer() { ... return inner; } |
| Generator Function | No | No | Yes | Maybe | function*(){ yield } |
| Pure Function | Maybe | Maybe | Maybe | No | f(x) => x * 2 |
| First-Class Function | Maybe | Maybe | Maybe | Maybe | arr.map(fn) |
| Immutability | N/A | N/A | N/A | No | [...arr, x] |
| Recursion | Maybe | Maybe | Maybe | Maybe | function f() { f(); } |
| Composition | Maybe | Maybe | Maybe | Maybe | f(g(x)) |
| Anonymous Function | Maybe | Maybe | Maybe | Maybe | (x) => x * 2 |
| Callback Function | Maybe | Yes | Maybe | Maybe | setTimeout(fn, t) |
| Memoization | Yes | Maybe | Yes | No | memoize(fn) |
In practice:
- Use currying for partial application and function composition.
- Use higher order functions for abstraction, callbacks, and code reuse.
- Use closures to maintain state and enable advanced function patterns.
- Use generator functions for lazy sequences, custom iteration, and advanced control flow.
- Use pure functions for predictable, testable code.
- Leverage first-class functions for callbacks, event handlers, and functional patterns.
- Apply immutability, recursion, composition, anonymous functions (lambdas), callbacks, and memoization for robust and maintainable JavaScript code.