What Is Function Composition?
Function Composition is the practice of combining simple functions into more complex ones.
In Math:
(f ∘ g)(x) = f(g(x))
Read as: “f after g” — first g, then f.
In Python:
def add_10(x):
return x + 10
def multiply_2(x):
return x * 2
# Composition: first add_10, then multiply_2
result = multiply_2(add_10(5))
print(result) # 30
# 5 → add_10 → 15 → multiply_2 → 30
Functions are like LEGO bricks — build complex things from simple pieces! 🧱
Why Use Composition?
Without composition (repetition):
# Processing data in 3 steps
data1 = step1(raw_data)
data2 = step2(data1)
result = step3(data2)
# We repeat this every time!
data1 = step1(other_data)
data2 = step2(data1)
result = step3(data2)
With composition (reuse):
# Build the pipeline once
process = compose(step1, step2, step3)
# Use it many times!
result1 = process(raw_data)
result2 = process(other_data)
result3 = process(more_data)
Simple compose() Function
def compose(f, g):
"""Compose two functions: f(g(x))."""
def composed(x):
return f(g(x))
return composed
# Example
def add_10(x):
return x + 10
def multiply_2(x):
return x * 2
# Create a new function
add_then_multiply = compose(multiply_2, add_10)
print(add_then_multiply(5)) # 30
# 5 → add_10 → 15 → multiply_2 → 30
Composing Many Functions
def compose(*functions):
"""Compose multiple functions."""
def composed(x):
result = x
for func in reversed(functions): # Right to left!
result = func(result)
return result
return composed
# Example
def add_10(x):
return x + 10
def multiply_2(x):
return x * 2
def square(x):
return x ** 2
# Composition: first add_10, then multiply_2, then square
pipeline = compose(square, multiply_2, add_10)
print(pipeline(5)) # 900
# 5 → add_10 → 15 → multiply_2 → 30 → square → 900
Reads right to left! (as in mathematics)
pipe — Composition Left to Right
def pipe(*functions):
"""Apply functions left to right."""
def piped(x):
result = x
for func in functions: # Left to right!
result = func(result)
return result
return piped
# Example (same result)
def add_10(x):
return x + 10
def multiply_2(x):
return x * 2
def square(x):
return x ** 2
# Pipe: add_10 → multiply_2 → square (reads naturally!)
pipeline = pipe(add_10, multiply_2, square)
print(pipeline(5)) # 900
# 5 → add_10 → 15 → multiply_2 → 30 → square → 900
Reads left to right! (more natural for code)
Practical Examples
Example 1: String Processing
def trim(text):
"""Strip whitespace."""
return text.strip()
def lowercase(text):
"""Convert to lowercase."""
return text.lower()
def remove_punctuation(text):
"""Remove punctuation marks."""
import string
return text.translate(str.maketrans("", "", string.punctuation))
# Composition
clean_text = pipe(trim, lowercase, remove_punctuation)
# Usage
dirty = " Hello, World! "
clean = clean_text(dirty)
print(clean) # "hello world"
Example 2: Number Processing
def validate_positive(x):
"""Ensure value is > 0."""
if x <= 0:
raise ValueError("Must be positive")
return x
def apply_discount(percent):
"""Apply a percentage discount."""
def discount(price):
return price * (1 - percent / 100)
return discount
def add_tax(percent):
"""Add a tax percentage."""
def tax(price):
return price * (1 + percent / 100)
return tax
def round_price(price):
"""Round to 2 decimal places."""
return round(price, 2)
# Composition: validate → 20% discount → 10% tax → round
calculate_price = pipe(
validate_positive,
apply_discount(20),
add_tax(10),
round_price
)
# Usage
base_price = 100
final_price = calculate_price(base_price)
print(final_price) # 88.0
# 100 → validate → 80 (discount) → 88 (tax) → 88.0 (rounded)
Example 3: List Processing
def filter_even(numbers):
"""Keep only even numbers."""
return [x for x in numbers if x % 2 == 0]
def double_all(numbers):
"""Double every element."""
return [x * 2 for x in numbers]
def sum_all(numbers):
"""Sum all elements."""
return sum(numbers)
# Composition
process_numbers = pipe(filter_even, double_all, sum_all)
# Usage
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = process_numbers(numbers)
print(result) # 60
# [1..10] → [2,4,6,8,10] → [4,8,12,16,20] → 60
Composition with Decorators
Decorators are composition too!
def uppercase_decorator(func):
"""Decorator: convert result to uppercase."""
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def exclaim_decorator(func):
"""Decorator: append exclamation marks."""
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"{result}!!!"
return wrapper
@exclaim_decorator
@uppercase_decorator
def greet(name):
return f"hello, {name}"
print(greet("Alice")) # "HELLO, ALICE!!!"
# greet → uppercase → exclaim
Partial Application
Creating specialized functions from general ones.
from functools import partial
def power(base, exponent):
"""Raise base to exponent."""
return base ** exponent
# Create specialized functions
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
# Composition with partial
def add(a, b):
return a + b
add_10 = partial(add, 10)
add_100 = partial(add, 100)
process = pipe(add_10, add_100)
print(process(5)) # 115 (5 + 10 + 100)
Currying
Transforming a multi-argument function into a chain of single-argument functions.
def curry(func):
"""Turn a function into a curried version."""
def curried(a):
def inner(b):
return func(a, b)
return inner
return curried
# Regular function
def add(a, b):
return a + b
# Curried
curried_add = curry(add)
# Usage
add_10 = curried_add(10)
print(add_10(5)) # 15
print(add_10(20)) # 30
# Composition with currying
add_5 = curried_add(5)
add_10 = curried_add(10)
add_both = pipe(add_5, add_10)
print(add_both(100)) # 115 (100 + 5 + 10)
Composition for AI Processing
Model Output Pipeline
def normalize(data):
"""Normalize to 0-1 range."""
max_val = max(data) if data else 1
return [x / max_val for x in data]
def apply_threshold(threshold):
"""Zero out values below the threshold."""
def thresholded(data):
return [x if x >= threshold else 0 for x in data]
return thresholded
def to_classes(data):
"""Convert to binary classes 0/1."""
return [1 if x > 0.5 else 0 for x in data]
def count_positives(data):
"""Count positive predictions."""
return sum(data)
# Composition: normalize → threshold 0.3 → classify → count
process_model_output = pipe(
normalize,
apply_threshold(0.3),
to_classes,
count_positives
)
# Usage
raw_scores = [45, 78, 23, 89, 56, 12, 90]
positive_count = process_model_output(raw_scores)
print(positive_count) # 4
# [45,78,23,89,56,12,90] → [0.5,0.87,0.26,0.99,0.62,0.13,1.0]
# → [0.5,0.87,0,0.99,0.62,0,1.0] → [0,1,0,1,1,0,1] → 4
The Composable Class
A convenient class for composition.
class Composable:
"""A function with a compose method."""
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def then(self, other):
"""Compose: self first, then other."""
def composed(*args, **kwargs):
return other(self(*args, **kwargs))
return Composable(composed)
# Usage
@Composable
def add_10(x):
return x + 10
@Composable
def multiply_2(x):
return x * 2
@Composable
def square(x):
return x ** 2
# Method chaining!
pipeline = add_10.then(multiply_2).then(square)
print(pipeline(5)) # 900
# 5 → add_10 → 15 → multiply_2 → 30 → square → 900
Debugging Composition
Execution Tracing
def trace(func):
"""Decorator for debugging."""
def wrapper(x):
result = func(x)
print(f"{func.__name__}({x}) = {result}")
return result
wrapper.__name__ = func.__name__
return wrapper
# Apply trace
@trace
def add_10(x):
return x + 10
@trace
def multiply_2(x):
return x * 2
@trace
def square(x):
return x ** 2
# Composition
pipeline = pipe(add_10, multiply_2, square)
print(pipeline(5))
# Output:
# add_10(5) = 15
# multiply_2(15) = 30
# square(30) = 900
# 900
Common Mistakes
Mistake 1: Wrong Function Order
def add_10(x):
return x + 10
def multiply_2(x):
return x * 2
# compose reads right to left!
wrong = compose(add_10, multiply_2)
print(wrong(5)) # 20 (5*2 + 10), NOT 30!
# pipe reads left to right!
right = pipe(add_10, multiply_2)
print(right(5)) # 30 ((5+10) * 2)
Mistake 2: Functions with Incompatible Signatures
def add(a, b): # 2 arguments!
return a + b
def square(x): # 1 argument!
return x ** 2
# ❌ ERROR
pipeline = pipe(add, square)
# square expects 1 argument, but add returns a number
# ✅ CORRECT: use partial or lambda
from functools import partial
add_10 = partial(add, 10) # Now takes 1 argument
pipeline = pipe(add_10, square)
print(pipeline(5)) # 225 ((5+10)^2)
Mistake 3: Side Effects
counter = 0
def increment_and_double(x):
global counter
counter += 1 # Side effect!
return x * 2
# Composing functions with side effects — BAD!
# Results become unpredictable
Summary
Function Composition — what it is:
- ✅ Combining simple functions into complex ones
- ✅ Reusing logic
- ✅ Readable pipelines
- ✅ Pure functions without side effects
Two approaches:
# compose: right to left (math convention)
compose(f, g, h)(x) → f(g(h(x)))
# pipe: left to right (code convention)
pipe(h, g, f)(x) → f(g(h(x)))
compose implementation:
def compose(*functions):
def composed(x):
result = x
for func in reversed(functions):
result = func(result)
return result
return composed
pipe implementation:
def pipe(*functions):
def piped(x):
result = x
for func in functions:
result = func(result)
return result
return piped
Usage:
# Define the steps
def step1(x): return x + 10
def step2(x): return x * 2
def step3(x): return x ** 2
# Build the pipeline
process = pipe(step1, step2, step3)
# Run it
result = process(5) # 900
What’s Next?
Now you know Function Composition! 🎉
Applications:
- 🏭 Data pipelines — chained data processing
- 🤖 AI workflows — processing model outputs
- 🧹 Data cleaning — composing filters
- 🔄 Transformations — multi-step conversions
Related topics:
- Decorators — wrappers for functions
- Higher-order functions
- Functional programming paradigm
Function composition is the foundation of elegant code! 🧩✨
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!