Module 01 Exercise Answers¶
Use these answers to review reasoning and evidence, not only final syntax. Several implementations are defensible when they preserve the same contracts.
Answer 1: Trace one value through the pure core¶
The cleaned value is:
CleanDoc(
doc_id="paper-7",
title="Substitution",
abstract="pure values compose",
categories="cs.PL",
)
With chunk size six, the slices are:
| Text | Start | End |
|---|---|---|
"pure v" |
0 | 6 |
"alues " |
6 | 12 |
"compos" |
12 | 18 |
"e" |
18 | 19 |
A focused test:
def test_trace_one_document() -> None:
raw = RawDoc(
doc_id="paper-7",
title="Substitution",
abstract=" Pure values compose ",
categories="cs.PL",
)
env = RagEnv(chunk_size=6)
cleaned = clean_doc(raw)
chunks = chunk_doc(cleaned, env)
embedded = [embed_chunk(chunk) for chunk in chunks]
completed = structural_dedup_chunks(embedded)
assert cleaned.abstract == "pure values compose"
assert [(chunk.text, chunk.start, chunk.end) for chunk in chunks] == [
("pure v", 0, 6),
("alues ", 6, 12),
("compos", 12, 18),
("e", 18, 19),
]
assert "".join(chunk.text for chunk in chunks) == cleaned.abstract
assert all(len(chunk.embedding) == 16 for chunk in embedded)
assert all(
0.0 <= value <= 1.0
for chunk in embedded
for value in chunk.embedding
)
assert structural_dedup_chunks(completed) == completed
Every transition after constructing raw and env is pure value work. Equal text
gets equal embeddings because embed_chunk hashes only chunk.text; it copies
identity and offsets into the returned Chunk but does not use them to derive the
vector.
Common wrong turns:
- predicting chunks from the unnormalized abstract;
- treating
endas inclusive; - claiming the vector is semantically meaningful;
- calling
full_ragon completed chunks.
The test proves this concrete trace and fixed-point output. It does not prove the laws for all documents; the property suite supplies broader evidence.
Answer 2: Repair a mixed transformation¶
Separate the pure batch transform:
from the effectful owner:
def clean_and_record_batch(
docs: list[RawDoc],
seen_ids: list[str],
) -> list[CleanDoc]:
cleaned = clean_batch(docs)
seen_ids.extend(doc.doc_id for doc in docs)
return cleaned
The list parameter makes the effect target visible, but
clean_and_record_batch is still impure because it mutates that list. The name and
signature expose rather than erase the effect.
def test_clean_batch_is_independent_of_audit_state() -> None:
docs = [
RawDoc("a", "A", " PURE ", "cs.PL"),
RawDoc("b", "B", "Values", "cs.PL"),
]
before = list(docs)
seen_ids: list[str] = []
first = clean_batch(docs)
second = clean_batch(docs)
assert first == second
assert docs == before
assert seen_ids == []
recorded = clean_and_record_batch(docs, seen_ids)
assert recorded == first
assert seen_ids == ["a", "b"]
The original fails substitution because replacing normalize_batch(docs) with its
returned list removes appends to the global seen_ids.
A common wrong turn is moving seen_ids into a closure or default parameter. That
changes where the hidden state lives, not whether it exists.
These tests demonstrate returned values, input preservation, and one explicit effect target. They do not prove the source has no other hidden reads; source and dependency review remain necessary.
Answer 3: Earn a typed composition¶
One defensible local adapter is:
from collections.abc import Callable
def chunk_count(env: RagEnv) -> Callable[[CleanDoc], int]:
def count(doc: CleanDoc) -> int:
return len(chunk_doc(doc, env))
return count
Use it:
Focused evidence:
def raw(abstract: str) -> RawDoc:
return RawDoc("doc", "title", abstract, "cs.PL")
assert count_chunks(raw("")) == 0
assert count_chunks(raw("12345")) == 1
assert count_chunks(raw("123456")) == 2
RagPipe(clean_doc).then(embed_chunk) is incompatible because .then needs a
Callable[[CleanDoc], C], while embed_chunk accepts
ChunkWithoutEmbedding.
The closure captures an immutable policy value, not input documents or mutable run
state. It is still a preview of Module 02's configurator pattern. The adapter is
useful for the exercise, but Module 01's production docs_to_embedded remains more
direct because the actual application needs chunks, not only their count.
The examples prove count behavior at three boundaries. They do not prove the full-text reconstruction law; keep the existing chunk property for that.
Answer 4: Test the effect boundary¶
The tracked file
tests/learning/test_module_01_shell_boundary.py is the starting evidence, not an
answer to copy into another location. Its four tests divide responsibility:
| Existing route | Distinct observation |
|---|---|
| two valid rows | parsed JSONL equals the pure core and input text remains unchanged |
| headers only | an empty domain result still creates an empty destination |
| missing required field | row construction becomes contextual ValueError before output creation |
| missing path | filesystem-open failure propagates before output creation |
Operation classification:
| Operation | Classification |
|---|---|
open and csv.DictReader consumption |
read effect |
RawDoc construction |
value construction at boundary |
full_rag |
pure transformation |
open, json.dump, and write |
write effect |
One useful extension checks non-ASCII preservation. It adds a serialization observation without redefining the core:
from dataclasses import asdict
import json
from pathlib import Path
from funcpipe_rag import RagEnv, RawDoc, full_rag, rag_shell
def test_shell_preserves_non_ascii_jsonl(tmp_path: Path) -> None:
source = tmp_path / "unicode.csv"
output = tmp_path / "chunks.jsonl"
source.write_text(
"doc_id,title,abstract,categories\n"
"död,Ångström,Funktionell sökning,cs.PL\n",
encoding="utf-8",
)
docs = [
RawDoc("död", "Ångström", "Funktionell sökning", "cs.PL"),
]
env = RagEnv(chunk_size=32)
rag_shell(env, str(source), str(output))
output_text = output.read_text(encoding="utf-8")
actual = [json.loads(line) for line in output_text.splitlines()]
expected = [
json.loads(json.dumps(asdict(chunk)))
for chunk in full_rag(docs, env)
]
assert actual == expected
assert "död" in output_text
The parsed-value assertion protects delegation and field content. The raw-text
assertion specifically protects ensure_ascii=False; JSON using Unicode escape
sequences would parse to the same values and would not satisfy that second
observation.
Common wrong turns:
- comparing JSON text formatting instead of parsed values;
- hard-coding expected embeddings rather than calling the pure core;
- expecting missing paths to be translated when the source lets
FileNotFoundErrorpropagate; - claiming the tests prove atomic output.
The existing valid test would catch copied shell domain logic if it diverged from
full_rag; the empty and failure tests alone would not. The Unicode extension proves
one encoding choice. None of these tests proves that a write failure leaves no partial
file, that arbitrary paths are safe, or that concurrent writers are coordinated.
Answer 5: Review a false fixed-point claim¶
The proposal is ill-typed:
The inner call returns list[Chunk], which the outer call cannot accept.
The correct completed-output law is:
The idempotent function is structural_dedup_chunks:
Canonical order independence, under unique document IDs:
A deterministic non-idempotent counterexample:
It always returns the same result for the same input, but repeated application keeps adding markers.
Common wrong turns:
- using "idempotent" as a synonym for deterministic;
- omitting the unique-ID assumption from the reversal property;
- claiming canonicalization proves semantic embedding quality;
- proving only that one already-sorted example remains sorted.
The fixed-point assertion proves completed output needs no further structural deduplication. The reversal assertion proves one ordering property for the given inputs. Neither proves effect safety, resource bounds, retrieval relevance, or correctness for every possible domain value.
Final self-review¶
Your answers should preserve these earlier contracts:
- exact positive integer configuration;
- immutable domain inputs;
- deterministic normalization and embedding;
- full text coverage;
- canonical structural output;
- one explicit file-effect owner.
Run both Module 01 proof surfaces again before moving to Module 02.