Skip to main content

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
Note

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:

function sum(a) {
return function(b) {
return function(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).
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)
  • 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
Note

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:

  • withLogging is 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:
    1. Logs the arguments it receives.
    2. Calls the original sum function with those arguments.
  • So, loggedSum(2, 3) logs Calling function with args:[2, 3] and returns sum(2, 3) which is 5.
  • loggedSum is 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, logger is 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" the count variable, even after outer has 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.
tip

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 yield keyword 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 next yield.
  • The function's state is saved between calls, so you can produce values on demand.
  • When there are no more yield statements, done becomes true.

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 async generators and for 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
Note

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 with yield.
  • 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/ConceptReturns Function?Uses Function as Arg?Uses Closure?Side Effects?Example Pattern
Higher Order FunctionSometimesSometimesSometimesMaybeg(fn) or g()()
CurryingYesNoYesNof(a)(b)(c)
ClosureSometimesNoYesMaybefunction outer() { ... return inner; }
Generator FunctionNoNoYesMaybefunction*(){ yield }
Pure FunctionMaybeMaybeMaybeNof(x) => x * 2
First-Class FunctionMaybeMaybeMaybeMaybearr.map(fn)
ImmutabilityN/AN/AN/ANo[...arr, x]
RecursionMaybeMaybeMaybeMaybefunction f() { f(); }
CompositionMaybeMaybeMaybeMaybef(g(x))
Anonymous FunctionMaybeMaybeMaybeMaybe(x) => x * 2
Callback FunctionMaybeYesMaybeMaybesetTimeout(fn, t)
MemoizationYesMaybeYesNomemoize(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.