Combinator Laws and Trade-Offs¶
Concept Position¶
flowchart TD
family["Python Programming"] --> program["Python Functional Programming"]
program --> module["Module 01: Purity, Substitution, and Local Reasoning"]
module --> concept["Combinator Laws and Trade-Offs"]
concept --> capstone["Capstone pressure point"]
flowchart TD
problem["Start with the design or failure question"] --> example["Study the worked example and trade-offs"]
example --> boundary["Name the boundary this page is trying to protect"]
boundary --> proof["Carry that question into code review or the capstone"]
Read the first diagram as a placement map: this page is one concept inside its parent module, not a detached essay, and the capstone is the pressure test for whether the idea holds. Read the second diagram as the working rhythm for the page: name the problem, study the example, identify the boundary, then carry one review question forward.
This lesson exists because building a helper library is not the same thing as justifying it. Once you have a small combinator kit, the next question is whether it preserves the behavior you care about and whether it stays simpler than the imperative baseline.
What this lesson proves¶
- the helper surface obeys identity, composition, and idempotence laws where expected
- property-based tests can falsify bad combinator refactors quickly
- a combinator library is only worth keeping when it reduces duplication and review cost
- order preservation and type clarity matter more than functional vocabulary on its own
Equational review route¶
Use substitution to check whether a combinator pipeline is still honest:
- inline the combinator definitions mentally or on paper
- confirm the rewritten pipeline still means "data in, data out"
- compare the result with the hand-written imperative baseline
- stop if the abstraction hides control flow instead of clarifying it
For Module 01, the most important question is not "can I write this point-free?" The important question is "can another engineer still substitute the pieces locally and trust the result?"
Property-based review route¶
Property tests are the fastest way to catch abstractions that look elegant but break important guarantees.
Good properties for this lesson include:
fmap(identity) == identityfmap(f ∘ g) == fmap(f) ∘ fmap(g)- applying the same filter twice is the same as applying it once
- left folds preserve associativity when the operation itself is associative
- the combinator pipeline and the hand-written pipeline return the same value
Bad refactor example¶
Order-preserving deduplication is a good example because the failure is subtle: a helper
that converts to set looks shorter but silently destroys ordering.
from typing import Callable, Iterable, List, TypeVar
T = TypeVar("T")
def bad_unique() -> Callable[[Iterable[T]], List[T]]:
def inner(xs: Iterable[T]) -> List[T]:
return list(set(xs))
return inner
That implementation is smaller, but it no longer matches the behavioral contract of a stable pipeline helper.
Better specification¶
from typing import List, TypeVar
T = TypeVar("T")
def stable_unique(xs: List[T]) -> List[T]:
seen = set()
out: List[T] = []
for x in xs:
if x not in seen:
seen.add(x)
out.append(x)
return out
The point is not only to get the code right. The point is to make the intended contract obvious enough that a test can guard it.
When the library is worth it¶
Keep the combinator layer when it gives you these benefits:
- the same pipeline pieces are reused in multiple places
- the helpers preserve types and naming clearly enough to review
- the tests describe stable algebraic expectations, not only examples
- a new pipeline becomes easier to assemble than a hand-written loop forest
Do not keep it when:
- the helper names are more abstract than the problem
- the pipeline is so small that direct code is clearer
- profiling shows the abstraction cost matters and the hot path is simple
- the team cannot explain the laws the library depends on
Capstone check¶
Before moving on, compare the combinator helpers in the capstone with the tests that justify them:
- inspect
capstone/_history/worktrees/module-01/src/funcpipe_rag/fp.py - inspect
capstone/_history/worktrees/module-01/tests/test_laws.py - decide whether each helper is buying clarity or just renaming normal Python
Reflection¶
- Which helper in your own codebase would survive a law-based review?
- Which helper only exists because earlier code was repetitive?
- Which helper looks elegant but makes the execution story harder to follow?
Continue with: Typed Pipelines