Skip to content

Higher-Order Functions & Closures

Overview

Higher-order functions work with other functions: - Accept functions as arguments: Customize behavior - Return functions: Create function factories - Closures: Inner functions capture outer scope - Partial application: "Lock in" some arguments - Currying: Convert multi-argument functions to chained single-argument functions


Higher-Order Functions

Functions as Arguments

# Higher-order function: takes function as argument
def apply_twice(func, x):
    """Apply function twice."""
    return func(func(x))

# Use with different functions
square = lambda x: x ** 2
add_one = lambda x: x + 1

print(apply_twice(square, 3))    # (3²)² = 81
print(apply_twice(add_one, 3))   # (3+1)+1 = 5

# Built-in higher-order functions
numbers = [1, 2, 3, 4, 5]

# map: Apply function to each element
squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # [1, 4, 9, 16, 25]

# filter: Keep elements where function returns True
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4]

# reduce: Accumulate with function
from functools import reduce
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total)  # 15

Practical Higher-Order Functions

def process_batch(data, *transforms):
    """Apply multiple transformations to data."""
    result = data
    for transform in transforms:
        result = transform(result)
    return result

# Use it
data = [1, 2, 3, 4, 5]

def double(x):
    return [i * 2 for i in x]

def filter_even(x):
    return [i for i in x if i % 2 == 0]

def sum_all(x):
    return sum(x)

result = process_batch(data, double, filter_even, sum_all)
print(result)  # [4] doubled → [2,4,6,8,10], filter → [4,8], sum → 12

Functions Returning Functions

Function Factories

# Factory that creates functions
def make_adder(n):
    """Create a function that adds n to its argument."""
    def adder(x):
        return x + n
    return adder

# Create specialized functions
add_5 = make_adder(5)
add_10 = make_adder(10)

print(add_5(3))   # 8
print(add_10(3))  # 13

# Factory for transformations
def make_scaler(factor):
    """Create scaling function."""
    return lambda x: x * factor

scale_2x = make_scaler(2)
scale_10x = make_scaler(10)

print(scale_2x(5))   # 10
print(scale_10x(5))  # 50

# Factory for composition
def make_pipeline(*operations):
    """Create pipeline of operations."""
    def pipeline(x):
        result = x
        for op in operations:
            result = op(result)
        return result
    return pipeline

normalize = lambda x: (x - x.mean()) / x.std()
scale = lambda x: x * 255
preprocess = make_pipeline(normalize, scale)

Closures

Capturing Outer Scope

# Closure: inner function captures outer variables
def outer(x):
    # This variable is captured by inner
    captured_value = x * 10

    def inner(y):
        # inner "closes over" captured_value
        return y + captured_value

    return inner

# Create closures with different captured values
add_10 = outer(1)   # Captures 1*10 = 10
add_50 = outer(5)   # Captures 5*10 = 50

print(add_10(3))    # 3 + 10 = 13
print(add_50(3))    # 3 + 50 = 53

# Practical closure: counter
def make_counter():
    """Create a counter with internal state."""
    count = 0

    def increment():
        nonlocal count  # Modify outer variable
        count += 1
        return count

    def get():
        return count

    return increment, get

inc1, get1 = make_counter()
inc2, get2 = make_counter()

print(inc1())  # 1
print(inc1())  # 2
print(inc2())  # 1 (separate counter!)
print(get1())  # 2
print(get2())  # 1

Closure with Configuration

# Closure captures configuration
def create_classifier(threshold):
    """Create classifier with specific threshold."""
    def classify(value):
        if value > threshold:
            return "high"
        else:
            return "low"
    return classify

# Different classifiers
high_threshold = create_classifier(0.8)
low_threshold = create_classifier(0.3)

print(high_threshold(0.9))  # "high"
print(high_threshold(0.5))  # "low"
print(low_threshold(0.5))   # "high"

Partial Application

Using functools.partial

from functools import partial

def power(base, exponent):
    """Compute base^exponent."""
    return base ** exponent

# Partial: "lock in" base argument
square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# More practical example
def format_string(template, *args, **kwargs):
    return template.format(*args, **kwargs)

greet = partial(format_string, "Hello, {}!")
print(greet("Alice"))  # "Hello, Alice!"
print(greet("Bob"))    # "Hello, Bob!"

# Practical ML use
def train_model(model, optimizer, data, learning_rate, epochs):
    for epoch in range(epochs):
        # training code
        pass

# Create trainer with fixed LR and epochs
train_with_adam = partial(train_model, optimizer='adam', learning_rate=0.001, epochs=100)
# Now only need to provide model and data

Currying

Converting Multi-Arg to Single-Arg Functions

# Curried function: takes one argument at a time
def curried_add(a):
    def with_b(b):
        def with_c(c):
            return a + b + c
        return with_c
    return with_b

# Use it
result = curried_add(1)(2)(3)  # 6

# Or use intermediate steps
add_one = curried_add(1)
add_one_and_two = add_one(2)
result = add_one_and_two(3)  # 6

# Currying utility
def curry(func):
    """Convert function to curried form."""
    from functools import wraps
    @wraps(func)
    def curried(*args, **kwargs):
        if len(args) + len(kwargs) >= func.__code__.co_argcount:
            return func(*args, **kwargs)
        return partial(curried, *args, **kwargs)
    return curried

@curry
def multiply(a, b, c):
    return a * b * c

print(multiply(2)(3)(4))  # 24
print(multiply(2, 3)(4))  # 24
print(multiply(2, 3, 4))  # 24

Decorators as Higher-Order Functions

Function Wrapping

from functools import wraps

# Decorator is a higher-order function
def timing_decorator(func):
    """Measure function execution time."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timing_decorator
def slow_function(n):
    total = 0
    for i in range(n):
        total += i ** 0.5
    return total

slow_function(1000000)
# Prints: slow_function took X.XXXXs

Parametrized Decorators

# Decorator factory returns decorator
def repeat(times):
    """Decorator that repeats function execution."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(times):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))
# Output: ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

Real-World ML Examples

Higher-Order Loss Function

import torch

def create_weighted_loss(weights):
    """Factory for weighted loss functions."""
    def loss_fn(predictions, targets):
        errors = (predictions - targets) ** 2
        weighted = errors * weights
        return weighted.mean()
    return loss_fn

# Different loss functions for different data
loss_balanced = create_weighted_loss(torch.ones(10))
loss_biased = create_weighted_loss(torch.tensor([1.0] * 5 + [0.5] * 5]))

Higher-Order Data Transform

def create_augmentation_pipeline(*augmentations):
    """Create data augmentation pipeline."""
    def augment(batch):
        result = batch
        for aug in augmentations:
            result = aug(result)
        return result
    return augment

# Create pipelines
rotate = lambda x: x  # placeholder
flip = lambda x: x    # placeholder
scale = lambda x: x   # placeholder

train_augment = create_augmentation_pipeline(rotate, flip, scale)
test_augment = create_augmentation_pipeline()  # No augmentation

Summary: Higher-Order Function Patterns

Pattern Use Case Example
map/filter Transform collections map(square, numbers)
Function factory Create specialized functions make_adder(5)
Closure Capture state/config create_classifier(0.8)
Partial application Fix arguments partial(func, x=10)
Currying Single-argument functions curry(func)(a)(b)
Decorators Wrap functions @decorator