Part 1 — Currying add
def add_c(x):
def inner(y):
return x + y
return inner
# or, more compactly:
add_c = lambda x: lambda y: x + y
add_c(3)(4) # 7
add10 = add_c(10)
add10(5) # 15
add10(100) # 110
add_c(10) returns the inner closure with x=10 captured. Every subsequent call just adds 10.
This is make_adder(10) from Lesson 13 — currying is the general pattern behind all those factories.
Part 2 — functools.partial
from functools import partial
square = partial(power, exponent=2) # or: partial(power, 2) for positional
cube = partial(power, exponent=3)
powers_of_2 = partial(power, 2)
square(5) # 25
cube(3) # 27
powers_of_2(8) # 256
Note: partial(power, 2) fixes the first positional argument (base=2). partial(power, exponent=2)
uses a keyword argument to fix the second. Both work; keyword form is clearer for non-first args.
Part 3 — compose and double_then_square
def compose(f, g):
def h(x):
return f(g(x))
return h
double = partial(power, exponent=1) # Hmm — power(base, 1) = base, not double
Wait — power(base, exponent) uses the base; to double we need multiplication, not power. Let’s
define double correctly:
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2) # multiply(2, x) = 2*x
square = partial(power, exponent=2) # power(x, 2) = x^2
double_then_square = compose(square, double)
double_then_square(3) # square(double(3)) = square(6) = 36
double_then_square(5) # square(double(5)) = square(10) = 100
compose(square, double)(x) = square(double(x)) = (2x)². Composition order matters: the
rightmost function (double) runs first.
Stretch — compose_all
def compose_all(fns):
"""Compose a list of functions right-to-left (last in list runs first)."""
def composed(x):
result = x
for f in reversed(fns):
result = f(result)
return result
return composed
Or, elegantly, using foldl from Lesson 15:
from functools import reduce
compose_all = lambda fns: reduce(compose, fns)
reduce(compose, [f, g, h]) computes compose(compose(f, g), h), which means h runs first
(right-to-left). For fns = [str, abs, lambda x: x-100]:
compose(str, abs) → str(abs(x))
compose(compose(str, abs), sub100) → str(abs(x - 100))
compose_all([str, abs, sub100])(42) = str(abs(42 - 100)) = str(|-58|) = str(58) = "58"
The big picture
Currying, partial application, and composition are the connective tissue of functional programming. They let you:
- Build specialised functions from general ones (partial application).
- Chain small transformations into pipelines (composition).
- Write functions in point-free style — defining a function entirely in terms of other functions, without naming the argument it operates on.
In Haskell and Scala, all functions are curried by default and . / andThen do composition. In
Python you opt in — but functools.partial and compose give you the same toolkit.
Next: the payoff. Use foldl to reconstruct map, filter, sum, and product — showing
that fold is the one abstraction that generates all the others (Lesson 18).