Skip to content

Higher-Order Functions

Interactive Pipeline Visualizer

Watch data flow through a chain of map, filter, and reduce operations. Modify the pipeline steps to see how each transformation affects the data.

Functional Pipeline Visualizer

Watch data flow through map, filter, reduce, and flatMap operations

Double all numbers, then keep only even results

Pipeline Chain
data
..map(x * 2)
..filter(even)
..reduce(sum)
Input
12345678
8 items
.map(x * 2)
...
.filter(even)
...
.reduce(sum)
...
Code Equivalent
const result = [1, 2, 3, 4, 5, 6, 7, 8]
  .map(x * 2)
  .filter(even)
  .reduce(sum);

A higher-order function is a function that takes one or more functions as arguments, returns a function, or both. Higher-order functions are the backbone of functional programming — they enable code reuse, abstraction, and composition at a level that is impossible with first-order functions alone.


First-Class Functions

Before diving into higher-order functions, we need to understand first-class functions. In a language with first-class functions, functions are values — they can be:

  • Assigned to variables
  • Stored in data structures
  • Passed as arguments to other functions
  • Returned from other functions
First-class functions:
greet = function(name) => "Hello, " + name
functions = [add, subtract, multiply] -- stored in a list
applyTwice(double, 5) -- passed as argument
returns 20
makeMultiplier(3) -- returns a function
=> function(x) => x * 3

The Big Three: Map, Filter, Reduce

These three higher-order functions replace the vast majority of explicit loops in functional code.

Map

Map applies a function to every element of a collection and returns a new collection with the results.

map(f, [a, b, c]) = [f(a), f(b), f(c)]
Input: [1, 2, 3, 4, 5]
Function: x => x * 2
Output: [2, 4, 6, 8, 10]

Filter

Filter keeps only the elements that satisfy a predicate (a function that returns true or false).

filter(p, [a, b, c, d]) = elements where p(x) is true
Input: [1, 2, 3, 4, 5, 6]
Predicate: x => x is even
Output: [2, 4, 6]

Reduce (Fold)

Reduce combines all elements into a single value by repeatedly applying a function that takes an accumulator and the current element.

reduce(f, init, [a, b, c]) = f(f(f(init, a), b), c)
Input: [1, 2, 3, 4, 5]
Function: (acc, x) => acc + x
Initial: 0
Steps: 0 + 1 = 1
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
Output: 15
from functools import reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# MAP: double every number
doubled = list(map(lambda x: x * 2, numbers))
# [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# Pythonic alternative: list comprehension
doubled = [x * 2 for x in numbers]
# FILTER: keep only even numbers
evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4, 6, 8, 10]
# Pythonic alternative
evens = [x for x in numbers if x % 2 == 0]
# REDUCE: sum all numbers
total = reduce(lambda acc, x: acc + x, numbers, 0)
# 55
# CHAINING: sum of squares of even numbers
result = reduce(
lambda acc, x: acc + x,
map(lambda x: x ** 2,
filter(lambda x: x % 2 == 0, numbers)),
0
)
# 220
# More readable with comprehension
result = sum(x ** 2 for x in numbers if x % 2 == 0)
# 220
# Real-world example: processing user records
users = [
{"name": "Alice", "age": 30, "active": True},
{"name": "Bob", "age": 25, "active": False},
{"name": "Charlie", "age": 35, "active": True},
{"name": "Diana", "age": 28, "active": True},
]
# Get names of active users over 27
active_names = [
user["name"]
for user in users
if user["active"] and user["age"] > 27
]
# ["Alice", "Charlie", "Diana"]

Other Useful Higher-Order Functions

FunctionDescriptionExample
flatMapMap then flatten nested collections[[1,2],[3,4]] -> [1,2,3,4]
find / firstReturn first element matching a predicateFind first even number
every / allTrue if all elements match the predicateAre all numbers positive?
some / anyTrue if any element matches the predicateIs any number negative?
zipCombine two lists element-wisezip([1,2], ['a','b']) -> [(1,'a'), (2,'b')]
groupByGroup elements by a key functionGroup users by country
sortBySort using a key-extraction functionSort users by age
takeWhileTake elements while predicate is trueTake while less than 5

Closures

A closure is a function that captures variables from its enclosing scope. The captured variables persist even after the enclosing function has returned.

Closure:
function makeCounter() {
let count = 0; ◄── captured variable
return function increment() {
count += 1; ◄── accesses captured variable
return count;
}
}
counter = makeCounter();
counter(); // 1 -- count persists between calls
counter(); // 2
counter(); // 3
# Closure: function that captures enclosing variables
def make_multiplier(factor):
"""Returns a function that multiplies by factor."""
def multiply(x):
return x * factor # 'factor' is captured
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Closure for configuration
def make_logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
info = make_logger("INFO")
error = make_logger("ERROR")
info("Server started") # [INFO] Server started
error("Connection lost") # [ERROR] Connection lost
# Closure for private state (counter)
def make_counter(initial=0):
count = [initial] # List for mutability in closure
def increment():
count[0] += 1
return count[0]
def get():
return count[0]
return increment, get
inc, get = make_counter()
inc(); inc(); inc()
print(get()) # 3

Currying

Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument.

Uncurried: add(a, b) = a + b -- takes 2 args at once
Curried: add(a)(b) = a + b -- takes 1 arg, returns function
add(2, 3) -- uncurried call
add(2)(3) -- curried call
add(2) returns a function: (b) => 2 + b
Then (b) => 2 + b is called with 3
Result: 5
# Manual currying
def add(a):
def inner(b):
return a + b
return inner
add_five = add(5)
print(add_five(3)) # 8
print(add(2)(3)) # 5
# Generic curry function
from functools import wraps
import inspect
def curry(fn):
"""Automatically curry a function."""
arity = len(inspect.signature(fn).parameters)
@wraps(fn)
def curried(*args):
if len(args) >= arity:
return fn(*args)
return lambda *more: curried(*args, *more)
return curried
@curry
def add_three(a, b, c):
return a + b + c
print(add_three(1)(2)(3)) # 6
print(add_three(1, 2)(3)) # 6
print(add_three(1)(2, 3)) # 6
print(add_three(1, 2, 3)) # 6
# Practical use: configurable data pipeline
@curry
def filter_by(key, value, records):
return [r for r in records if r.get(key) == value]
@curry
def pluck(key, records):
return [r[key] for r in records]
users = [
{"name": "Alice", "role": "admin"},
{"name": "Bob", "role": "user"},
{"name": "Carol", "role": "admin"},
]
get_admins = filter_by("role", "admin")
get_names = pluck("name")
admin_names = get_names(get_admins(users))
# ["Alice", "Carol"]

Partial Application

Partial application fixes some arguments of a function, producing a new function with fewer parameters. Unlike currying (which always produces unary functions), partial application can fix any number of arguments at once.

Currying vs Partial Application:
Original: f(a, b, c)
Curried: f(a)(b)(c) -- always one arg at a time
Partial: f(a, b)(c) -- fix some args, any number
f(a)(b, c) -- or different combinations
from functools import partial
def power(base, exponent):
return base ** exponent
# Partial application: fix the exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(3)) # 27
# Real-world: configuring a logger
import logging
def log_message(level, category, message):
logging.log(level, f"[{category}] {message}")
# Create specialized loggers
log_error = partial(log_message, logging.ERROR)
log_db_error = partial(log_message, logging.ERROR, "DATABASE")
log_error("AUTH", "Invalid token")
log_db_error("Connection timeout")
# Partial application with map
def multiply(a, b):
return a * b
double_all = partial(map, partial(multiply, 2))
print(list(double_all([1, 2, 3, 4]))) # [2, 4, 6, 8]

Writing Your Own Higher-Order Functions

import time
from functools import wraps
# Retry: higher-order function that adds retry logic
def retry(max_attempts=3, delay=1.0):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return fn(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
import requests
return requests.get(url).json()
# Timing decorator
def timed(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_operation():
time.sleep(1)
return "done"

Summary

ConceptKey Takeaway
Higher-order functionTakes functions as arguments or returns functions
MapTransform every element: [a, b, c] -> [f(a), f(b), f(c)]
FilterKeep elements matching a predicate
ReduceCollapse a collection into a single value
ClosureFunction that captures variables from its enclosing scope
CurryingTransform f(a, b, c) into f(a)(b)(c)
Partial applicationFix some arguments to create a specialized function
CompositionCombine simple functions into complex transformations