Pure Functions and Contracts¶
Purity becomes useful in engineering when it is stated as a contract another person can review:
That sentence is stronger than "this function looks functional." It identifies a domain, an output promise, and an effect promise.
Three layers of contract¶
Review each pure function at three layers.
| Layer | Question | FuncPipe example |
|---|---|---|
| shape | What values may cross the boundary? | chunk_doc accepts CleanDoc and RagEnv |
| behavior | How is the output related to the input? | chunk texts concatenate to the cleaned abstract |
| effects | What external state may be observed or changed? | chunk_doc performs no I/O or mutation |
Type annotations help with shape, but Python does not enforce them at runtime.
RagEnv therefore validates its own invariant:
@dataclass(frozen=True)
class RagEnv:
chunk_size: int
def __post_init__(self) -> None:
if type(self.chunk_size) is not int:
raise ValueError("RagEnv.chunk_size must be an int")
if self.chunk_size <= 0:
raise ValueError("RagEnv.chunk_size must be a positive integer")
The exact-type check is deliberate. True participates in integer arithmetic in
Python, but it is not a defensible chunk-size value.
Valid input matters¶
The Module 01 stages are total over their stated domain: for valid domain values they return a value instead of selecting an effectful failure route. That does not mean every arbitrary Python object is accepted.
RagEnv(chunk_size=4) # valid
RagEnv(chunk_size=0) # ValueError: violates the positive-size invariant
RagEnv(chunk_size=True) # ValueError: bool is not an exact int here
Separating construction from transformation simplifies every downstream function.
chunk_doc may rely on a positive exact integer because invalid RagEnv values
cannot be constructed normally.
Do not overstate this guarantee as "pure functions never raise." A pure function can raise deterministically. The better design question is whether failure belongs in domain construction, in an explicit result value, or at an effectful boundary. Later modules introduce richer failure values.
Read the actual stage contracts¶
The completed Module 01 stages live in:
Their important contracts are:
clean_doc¶
- preserves
doc_id,title, andcategories; - strips, lower-cases, and collapses whitespace in
abstract; - returns a new frozen
CleanDoc; - accepts a
CleanDocso its fixed-point law is also true in the type contract.
This is idempotence, not merely determinism.
chunk_doc¶
- preserves the document ID;
- emits non-overlapping slices in increasing offset order;
- records inclusive
startand exclusiveend; - covers the whole abstract, with a possibly shorter final slice;
- returns an empty list for an empty abstract.
For Module 01's non-overlapping policy:
cleaned = clean_doc(raw)
chunks = chunk_doc(cleaned, RagEnv(chunk_size=5))
assert "".join(chunk.text for chunk in chunks) == cleaned.abstract
assert all(chunk.text == cleaned.abstract[chunk.start:chunk.end] for chunk in chunks)
embed_chunk¶
- copies identity, text, and offsets unchanged;
- derives a 16-element tuple from
chunk.text; - produces the same embedding for equal text regardless of document ID or position.
The embedding is a teaching double. It gives deterministic local behavior; it does not claim semantic similarity.
structural_dedup_chunks¶
- sorts by
(doc_id, start); - removes repeated structural positions;
- reaches a fixed point after one call.
Its duplicate key is (doc_id, text, start, end). Read that key before claiming what
"duplicate" means. Arbitrary Chunk values with the same structure but conflicting
embeddings are outside normal production output; the first canonical-order value is
retained.
Contract violations that look plausible¶
Hidden policy¶
The shape annotation omits an input that determines output. Two identical-looking calls can mean different things after a global assignment.
Mutation before return¶
def destructive_clean(doc: dict[str, str]) -> dict[str, str]:
doc["abstract"] = " ".join(doc["abstract"].split())
return doc
Even if the returned mapping has the right content, callers holding the original reference observe an effect.
Logging inside a transform¶
def noisy_embed(chunk: ChunkWithoutEmbedding) -> Chunk:
print(f"embedding {chunk.doc_id}")
return embed_chunk(chunk)
The returned value remains deterministic, but replacing the call with its value removes output. The function is not referentially transparent.
A test that proves too little¶
This could pass while offsets overlap, characters disappear, or slices are reordered. A reconstruction assertion tests the behavioral relationship that matters.
Proof strategy¶
Examples and properties answer different questions:
- a named example explains a specific contract to a learner;
- a property test explores that contract over many generated values;
- a boundary example demonstrates how invalid values fail.
Module 01 uses both:
Run the short proof:
PYTHONPATH=capstone/module-reference-states/module-01/src \
python -m pytest -q \
capstone/module-reference-states/module-01/tests/learning/test_module_01_purity_foundations.py
Then inspect these named properties in tests/test_laws.py:
test_clean_doc_is_idempotenttest_chunk_doc_preserves_texttest_embed_chunk_depends_only_on_texttest_full_rag_is_canonical
The tests establish observed behavior for the explored domain. They do not prove that the implementation can never perform an effect; that claim also requires source and dependency review.
Review exercise¶
For one Module 01 stage, write a contract with:
- valid input domain;
- output shape;
- preserved fields;
- changed fields;
- effect promise;
- one invalid input route;
- one example assertion;
- one property that would catch a realistic regression.
Compare your contract with the source and both test surfaces. If the prose promises more than the code or the test proves less than the prose, record the mismatch rather than rationalizing it.
Continue with Immutability and Value Semantics to see why these contracts are easier to preserve when values cannot be changed behind another reference.