···17171818Jump to the [Concepts](./concepts/) section for deep-dives into specific topics. Each article is self-contained and includes real-world examples.
19192020+### Prefer Learning Through Narrative?
2121+2222+Try the [Stories](./stories/) — whimsical tales where animal characters discover functional programming concepts by building real software. Each story is a gentle introduction that weaves code into narrative.
2323+2024---
21252226## [Tutorial](./tutorial/)
···5559| [Fiber Internals](./concepts/07-fiber-internals.md) | How the runtime works under the hood |
5660| [Dependency Injection Patterns](./concepts/08-dependency-injection-patterns.md) | Testing, layered environments |
5761| [Testing Strategies](./concepts/09-testing-strategies.md) | Unit testing effectful code |
6262+6363+---
6464+6565+## [Stories](./stories/)
6666+6767+Learn through narrative — whimsical tales where animal characters discover FP concepts.
6868+6969+| Story | Concept | Description |
7070+|--------------------------------------------------------------------|--------------------------|------------------------------------------------|
7171+| [The Forest Election](./stories/forest-election/) | Validation, Result, Effects | Building a fair election system for the forest |
58725973---
6074
+45
docs/guides/stories/README.md
···11+# Stories
22+33+**Stories** are narrative-driven functional programming tutorials. Each story is a whimsical tale where animal characters discover and build the same concepts you'll use in production code.
44+55+---
66+77+## Why Stories?
88+99+Traditional tutorials show you *what* to type. Stories show you *why* these patterns exist — through characters who face the same problems you do and discover the same solutions.
1010+1111+When Rabbit worries "But what if someone votes twice?", you feel the problem. When Owl scratches out a `BallotError` type, you understand why typed errors matter. The code isn't an afterthought; it's woven into the narrative as the characters write it.
1212+1313+---
1414+1515+## How to Read
1616+1717+1. **Follow the narrative** — Let the story carry you through the concepts
1818+2. **Read the code** — Characters write real, runnable TypeScript
1919+3. **Run the example** — Each story has a matching file in `examples/stories/`
2020+4. **Try variations** — Modify the example to solidify your understanding
2121+2222+---
2323+2424+## Available Stories
2525+2626+### [The Forest Election](./forest-election/)
2727+2828+A trilogy about building a fair election system for the forest.
2929+3030+| Part | Title | Concept | Status |
3131+|------|--------------------------------------------------------------------------|------------|-----------|
3232+| 1 | [The Ballot Box Problem](./forest-election/01-the-ballot-box-problem.md) | Validation | Available |
3333+| 2 | [Counting Day](./forest-election/02-counting-day.md) | Result | Available |
3434+| 3 | [The Announcement](./forest-election/03-the-announcement.md) | Effects | Available |
3535+3636+**Tone:** Cozy and earnest (think Frog and Toad, Winnie the Pooh). The animals genuinely care about building fair software.
3737+3838+**Characters:** Owl (methodical thinker), Beaver (eager builder), Fox (pragmatic skeptic), Rabbit (anxious but thorough), Badger (wise elder).
3939+4040+---
4141+4242+## See Also
4343+4444+- [Tutorial](../tutorial/) — Step-by-step guide building a complete application
4545+- [Concepts](../concepts/) — Deep-dive reference articles
···11+# The Ballot Box Problem
22+33+*Part 1 of The Forest Election*
44+55+> In which the animals learn that finding ALL the problems
66+> is better than stopping at the first one.
77+88+---
99+1010+The clearing was already crowded when Beaver arrived. Animals of every size had gathered beneath the ancient oak where forest business was conducted. Today was special — election day.
1111+1212+"Every five years," Badger was saying to a group of young hedgehogs, "we choose who speaks for the forest. It's how we've always done it."
1313+1414+Beaver pushed through to the front where the candidates stood: Deer, calm and diplomatic; Squirrel, practically vibrating with energy; and Heron, who said little but saw everything.
1515+1616+At the base of the oak sat the ballot box — really just a hollow log with a slit carved in the top. A pile of leaves waited beside it.
1717+1818+"The process is simple," Badger announced. "Write your name on a leaf, write your choice, place it in the box."
1919+2020+Fox leaned against a nearby tree, arms crossed. "Same as last time, then."
2121+2222+Something in his tone made Rabbit's ears twitch. "What happened last time?"
2323+2424+Badger sighed. "The Great Squirrel Dispute. Two squirrels claimed the same ballot. We couldn't tell who'd actually voted. It... did not end well."
2525+2626+---
2727+2828+The voting began. Animals scratched names on leaves and dropped them through the slit. Everything seemed orderly until Owl, who had been tasked with the count, tipped the box and began sorting.
2929+3030+"This one is blank," she said, setting a leaf aside. "No voter name."
3131+3232+"And this one..." She squinted. "The candidate says 'Wolfe.' We don't have a candidate named Wolfe."
3333+3434+"Maybe they meant Wolf?" suggested Beaver.
3535+3636+"Wolf isn't running either."
3737+3838+More leaves spilled out. Owl's pile of problems grew.
3939+4040+"Here's one from Rabbit, voting for Deer. And here's *another* one from Rabbit, also voting for Deer."
4141+4242+"I was nervous!" Rabbit squeaked. "I couldn't remember if I'd already voted!"
4343+4444+"And this one..." Owl turned the leaf over twice. "Has no candidate at all. Just a drawing of an acorn."
4545+4646+Fox pushed off from his tree. "Simple solution. Throw out the bad ones, count what's left."
4747+4848+"But then we don't know what went wrong!" Rabbit wrung her paws. "What if someone's vote was supposed to count but we rejected it? What if we could have *fixed* it?"
4949+5050+"Rabbit has a point," said Owl. "If we only stop at the first problem, we might miss others. Someone fixes their blank name, submits again, and *then* we tell them their candidate doesn't exist?"
5151+5252+"So what do you suggest?" Fox asked.
5353+5454+Owl smoothed a fresh leaf with her wing. "Let me show you something."
5555+5656+---
5757+5858+"A ballot," said Owl, scratching careful marks, "has structure. It's not just any leaf — it's a leaf with *requirements*."
5959+6060+```typescript
6161+type Candidate = "Deer" | "Squirrel" | "Heron"
6262+6363+type Ballot = {
6464+ readonly voter: string
6565+ readonly candidate: Candidate
6666+}
6767+```
6868+6969+"A voter name," she continued. "And a candidate that must be one of our three."
7070+7171+Rabbit peered at the scratches. "But what happens when something's wrong?"
7272+7373+Owl made a new leaf. "Then we need to describe what's wrong. And there can be *multiple* things wrong with a single ballot."
7474+7575+```typescript
7676+type BallotError =
7777+ | { readonly _tag: "MissingVoter" }
7878+ | { readonly _tag: "MissingCandidate" }
7979+ | { readonly _tag: "InvalidCandidate"; readonly name: string }
8080+ | { readonly _tag: "DuplicateVoter"; readonly voter: string }
8181+```
8282+8383+"Missing voter," Rabbit said, reading along. "Missing candidate. Invalid candidate — that's the Wolfe one. And... duplicate voter." She looked sheepish.
8484+8585+"Now," said Owl, "the question is: when we find a problem, do we stop? Or do we keep looking?"
8686+8787+---
8888+8989+"Here's what most approaches do." Owl drew a diagram in the dirt.
9090+9191+```
9292+Check voter → Check candidate → Valid ballot
9393+ ↓ ↓
9494+ Stop Stop
9595+ (error) (error)
9696+```
9797+9898+"If the voter is missing, we stop. We never check the candidate. The submitter fixes the voter, resubmits, *then* discovers the candidate problem."
9999+100100+"That's what Fox said," Beaver observed.
101101+102102+"And it's efficient," Fox said. "Why waste time checking more if it's already broken?"
103103+104104+"Because," Rabbit said quietly, "it's frustrating. You fix one thing, you find another. Fix that, find a third. How many trips back and forth?"
105105+106106+Owl nodded. "What if instead..."
107107+108108+```
109109+Check voter ──────┐
110110+ │
111111+Check candidate ──┼──→ Collect all problems
112112+ │
113113+Check duplicate ──┘
114114+```
115115+116116+"We check everything. Collect every problem. Report them all at once."
117117+118118+---
119119+120120+"In type terms," Owl continued, "this is the difference between `Result` and `Validation`."
121121+122122+She scratched new marks:
123123+124124+```typescript
125125+// Result stops at the first error
126126+// Validation collects ALL errors
127127+128128+import {
129129+ type Validation,
130130+ valid,
131131+ invalidOne,
132132+ apValidation,
133133+ matchValidation,
134134+ pipe,
135135+} from "purus-ts"
136136+```
137137+138138+"A `Validation` is either valid, containing a value, or invalid, containing a *list* of errors. Not one error — a list."
139139+140140+"Show us how it works," said Beaver.
141141+142142+---
143143+144144+Owl wrote the first validator:
145145+146146+```typescript
147147+const validateVoter = (
148148+ voter: string | undefined
149149+): Validation<string, BallotError> =>
150150+ voter && voter.trim().length > 0
151151+ ? valid(voter.trim())
152152+ : invalidOne({ _tag: "MissingVoter" })
153153+```
154154+155155+"If the voter exists and isn't blank, it's `valid`. Otherwise, `invalidOne` creates an invalid result with that single error."
156156+157157+"I could write one for candidates," Beaver offered.
158158+159159+"Please," said Owl.
160160+161161+Beaver took a leaf:
162162+163163+```typescript
164164+const VALID_CANDIDATES: readonly Candidate[] = ["Deer", "Squirrel", "Heron"]
165165+166166+const validateCandidate = (
167167+ candidate: string | undefined
168168+): Validation<Candidate, BallotError> =>
169169+ !candidate || candidate.trim().length === 0
170170+ ? invalidOne({ _tag: "MissingCandidate" })
171171+ : VALID_CANDIDATES.includes(candidate as Candidate)
172172+ ? valid(candidate as Candidate)
173173+ : invalidOne({ _tag: "InvalidCandidate", name: candidate })
174174+```
175175+176176+"Good," said Owl. "If blank, missing. If not a valid candidate name, invalid. Otherwise, valid."
177177+178178+---
179179+180180+"But we have two validators," said Fox. "How do they become one?"
181181+182182+"This is where it gets interesting." Owl cleared a fresh patch of dirt. "We need to *combine* them. And the combining is what accumulates errors."
183183+184184+She wrote:
185185+186186+```typescript
187187+const makeBallot =
188188+ (voter: string) =>
189189+ (candidate: Candidate): Ballot => ({
190190+ voter,
191191+ candidate,
192192+ })
193193+194194+const validateBallot = (
195195+ voter: string | undefined,
196196+ candidate: string | undefined
197197+): Validation<Ballot, BallotError> =>
198198+ pipe(
199199+ valid(makeBallot),
200200+ apValidation(validateVoter(voter)),
201201+ apValidation(validateCandidate(candidate))
202202+ )
203203+```
204204+205205+"Start with `valid(makeBallot)` — a validation containing a function that *builds* a ballot. Then `apValidation` applies each validator in turn."
206206+207207+"The magic," she tapped the leaf, "is what happens when things go wrong."
208208+209209+---
210210+211211+"Let me trace through." Owl made four columns:
212212+213213+```
214214+Case 1: Both valid
215215+ valid(makeBallot) → Validation<(v) => (c) => Ballot>
216216+ ap(valid("Rabbit")) → Validation<(c) => Ballot>
217217+ ap(valid("Deer")) → Validation<Ballot>
218218+ Result: Valid({ voter: "Rabbit", candidate: "Deer" })
219219+220220+Case 2: Voter invalid
221221+ valid(makeBallot) → Validation<function>
222222+ ap(invalid([MissingVoter])) → Validation<function> with errors
223223+ ap(valid("Deer")) → still Invalid([MissingVoter])
224224+ Result: Invalid([MissingVoter])
225225+226226+Case 3: Both invalid
227227+ valid(makeBallot) → Validation<function>
228228+ ap(invalid([MissingVoter])) → Invalid([MissingVoter])
229229+ ap(invalid([MissingCand])) → Invalid([MissingVoter, MissingCand])
230230+ Result: Invalid([MissingVoter, MissingCandidate])
231231+```
232232+233233+"In case 3, both errors are collected! That's accumulation."
234234+235235+Rabbit's eyes widened. "So we know *everything* that's wrong. All at once."
236236+237237+"Exactly."
238238+239239+---
240240+241241+Fox had been studying the scratches. "What about duplicates? Rabbit voted twice."
242242+243243+"We need to track who's voted." Owl thought for a moment. "That's state — we'll need to check against a list."
244244+245245+```typescript
246246+const validateNoDuplicate = (
247247+ voter: string,
248248+ alreadyVoted: ReadonlySet<string>
249249+): Validation<string, BallotError> =>
250250+ alreadyVoted.has(voter)
251251+ ? invalidOne({ _tag: "DuplicateVoter", voter })
252252+ : valid(voter)
253253+```
254254+255255+"And we can combine this with the others." She rewrote the full validator:
256256+257257+```typescript
258258+const validateBallotFull = (
259259+ voter: string | undefined,
260260+ candidate: string | undefined,
261261+ alreadyVoted: ReadonlySet<string>
262262+): Validation<Ballot, BallotError> =>
263263+ pipe(
264264+ valid(makeBallot),
265265+ apValidation(
266266+ pipe(
267267+ validateVoter(voter),
268268+ // Only check duplicate if voter exists
269269+ (v) =>
270270+ v._tag === "Valid"
271271+ ? pipe(
272272+ validateNoDuplicate(v.value, alreadyVoted),
273273+ // Keep the voter value if no duplicate
274274+ (dup) => (dup._tag === "Valid" ? v : dup)
275275+ )
276276+ : v
277277+ )
278278+ ),
279279+ apValidation(validateCandidate(candidate))
280280+ )
281281+```
282282+283283+"Hmm," said Beaver. "That duplicate check is a bit tangled."
284284+285285+"It is," Owl admitted. "There's an alternative — run all validators independently and combine errors manually. For now, this works."
286286+287287+---
288288+289289+"Show us the full thing running," said Fox. "Prove it works."
290290+291291+Owl gathered her leaves:
292292+293293+```typescript
294294+const formatError = (error: BallotError): string => {
295295+ switch (error._tag) {
296296+ case "MissingVoter":
297297+ return "Ballot is missing a voter name"
298298+ case "MissingCandidate":
299299+ return "Ballot is missing a candidate"
300300+ case "InvalidCandidate":
301301+ return `"${error.name}" is not a valid candidate`
302302+ case "DuplicateVoter":
303303+ return `${error.voter} has already voted`
304304+ }
305305+}
306306+307307+// Test ballots
308308+const ballots = [
309309+ { voter: "Rabbit", candidate: "Deer" }, // Valid
310310+ { voter: undefined, candidate: "Squirrel" }, // Missing voter
311311+ { voter: "Fox", candidate: undefined }, // Missing candidate
312312+ { voter: "Beaver", candidate: "Wolfe" }, // Invalid candidate
313313+ { voter: undefined, candidate: undefined }, // Both missing!
314314+ { voter: "Rabbit", candidate: "Heron" }, // Duplicate voter
315315+]
316316+317317+const alreadyVoted = new Set<string>()
318318+319319+ballots.forEach(({ voter, candidate }, i) => {
320320+ const result = validateBallotFull(voter, candidate, alreadyVoted)
321321+322322+ matchValidation(
323323+ (ballot) => {
324324+ console.log(`Ballot ${i + 1}: Valid - ${ballot.voter} votes for ${ballot.candidate}`)
325325+ alreadyVoted.add(ballot.voter)
326326+ },
327327+ (errors) => {
328328+ console.log(`Ballot ${i + 1}: Invalid`)
329329+ errors.forEach((e) => console.log(` - ${formatError(e)}`))
330330+ }
331331+ )(result)
332332+})
333333+```
334334+335335+Owl ran through the leaves. The results appeared in the dirt:
336336+337337+```
338338+Ballot 1: Valid - Rabbit votes for Deer
339339+Ballot 2: Invalid
340340+ - Ballot is missing a voter name
341341+Ballot 3: Invalid
342342+ - Ballot is missing a candidate
343343+Ballot 4: Invalid
344344+ - "Wolfe" is not a valid candidate
345345+Ballot 5: Invalid
346346+ - Ballot is missing a voter name
347347+ - Ballot is missing a candidate
348348+Ballot 6: Invalid
349349+ - Rabbit has already voted
350350+```
351351+352352+"Ballot 5!" Beaver pointed excitedly. "Two errors at once!"
353353+354354+"That's the one that would have needed two trips in the old way," said Rabbit. "Fix the voter, submit, then find out about the candidate."
355355+356356+"Now they know everything immediately."
357357+358358+---
359359+360360+Fox nodded slowly. "I'll admit, this is better than I expected."
361361+362362+"It's not about complexity," said Owl. "It's about respect. Respect for whoever is submitting. Tell them everything wrong at once. Let them fix it all in one go."
363363+364364+Badger had been quiet, watching. Now he spoke. "We have valid ballots and invalid ones. The invalid ones are clearly marked. We can set them aside, ask those voters to try again with corrections."
365365+366366+"And the valid ones are *truly* valid," added Rabbit. "We know they have a real voter, a real candidate, and no duplicates."
367367+368368+The sun was lower now. The pile of leaves had been sorted — valid on one side, invalid on the other with their errors carefully noted.
369369+370370+"The validation is done," said Badger. "Tomorrow, we count. And counting," he added with a slight smile, "can also go wrong."
371371+372372+Owl began gathering her notes. "Then we'll need a different tool for that. But that's tomorrow's problem."
373373+374374+The animals dispersed as evening fell. The ballot box sat beneath the oak, its contents finally trustworthy.
375375+376376+---
377377+378378+## What We Learned
379379+380380+1. **Validation accumulates errors** — Unlike `Result` which stops at the first error, `Validation` collects all of them.
381381+382382+2. **Use `valid` and `invalidOne`** — Constructors for success and single-error failure cases.
383383+384384+3. **Combine with `apValidation`** — Apply validations to a curried constructor. Errors from all steps are merged.
385385+386386+4. **Handle with `matchValidation`** — Pattern match on the result to handle valid values or error lists.
387387+388388+5. **Typed errors are documentation** — `BallotError` with its `_tag` variants makes it clear exactly what can go wrong.
389389+390390+---
391391+392392+## Try It Yourself
393393+394394+The complete code from this story is available as a runnable example:
395395+396396+```bash
397397+bun examples/stories/forest-election/01-validation.ts
398398+```
399399+400400+Try modifying the test ballots to see different error combinations.
401401+402402+---
403403+404404+*Next: [Counting Day](./02-counting-day.md) (coming soon)*
···11+# Counting Day
22+33+*Part 2 of The Forest Election*
44+55+> In which the animals learn that some operations must stop
66+> at the first sign of trouble.
77+88+---
99+1010+Morning light filtered through the oak's branches. The animals gathered again, quieter now. Yesterday's chaos had been tamed — the validated ballots sat in one neat pile, each one guaranteed to have a real voter, a valid candidate, and no duplicates.
1111+1212+"Now we count," said Badger.
1313+1414+"Finally, the easy part," Fox said, settling onto a log.
1515+1616+Owl tilted her head. "Is it?"
1717+1818+---
1919+2020+Beaver had already begun sorting ballots into three piles — one for each candidate. Deer's pile. Squirrel's pile. Heron's pile.
2121+2222+"Twelve for Deer," he announced. "Nine for Squirrel. Seven for Heron."
2323+2424+"Twenty-eight total," said Rabbit, who had been counting ballots as they came out of the box.
2525+2626+Beaver paused. "I count twenty-eight in my piles too."
2727+2828+"So we agree," said Badger. "Good."
2929+3030+"Wait." Owl had been watching silently. "What if the totals hadn't matched?"
3131+3232+Fox shrugged. "Recount."
3333+3434+"But which count was wrong? The pile sort or the box count? And what if we'd already announced a result based on corrupted data?"
3535+3636+---
3737+3838+Rabbit's ears flattened. "You mean... the count could be wrong and we wouldn't know?"
3939+4040+"The validation caught problems with individual ballots," said Owl. "But the *counting process* can have its own problems. What if a ballot falls between the piles? What if someone miscounts? What if the box total doesn't match the pile totals?"
4141+4242+"Yesterday's lesson was about collecting all errors," Beaver said slowly. "But counting is different. If my pile total doesn't match the box total, I can't just... keep counting."
4343+4444+"Exactly." Owl smoothed a fresh leaf. "Some operations need to *stop* at the first error. Let me show you."
4545+4646+---
4747+4848+```typescript
4949+import {
5050+ type Result,
5151+ ok,
5252+ err,
5353+ chainResult,
5454+ mapResult,
5555+ matchResult,
5656+ pipe,
5757+} from "purus-ts"
5858+```
5959+6060+"Yesterday we used `Validation` which accumulates errors. Today we use `Result` which stops at the first one."
6161+6262+```typescript
6363+type Candidate = "Deer" | "Squirrel" | "Heron"
6464+6565+type Ballot = {
6666+ readonly voter: string
6767+ readonly candidate: Candidate
6868+}
6969+7070+type CountError =
7171+ | { readonly _tag: "EmptyBallotBox" }
7272+ | { readonly _tag: "TotalMismatch"; readonly expected: number; readonly actual: number }
7373+ | { readonly _tag: "TieDetected"; readonly candidates: readonly Candidate[]; readonly votes: number }
7474+```
7575+7676+"Three things can go wrong during counting," Owl explained. "The box might be empty. The totals might not match. Or we might have a tie."
7777+7878+---
7979+8080+"Let's start with the simplest operation," said Beaver. "Counting ballots for one candidate."
8181+8282+```typescript
8383+type VoteCounts = {
8484+ readonly Deer: number
8585+ readonly Squirrel: number
8686+ readonly Heron: number
8787+}
8888+8989+const countVotes = (ballots: readonly Ballot[]): VoteCounts =>
9090+ ballots.reduce<VoteCounts>(
9191+ (counts, ballot) => ({
9292+ ...counts,
9393+ [ballot.candidate]: counts[ballot.candidate] + 1,
9494+ }),
9595+ { Deer: 0, Squirrel: 0, Heron: 0 }
9696+ )
9797+```
9898+9999+"That can't fail," Fox observed.
100100+101101+"Not by itself, no. But watch what happens when we combine steps."
102102+103103+---
104104+105105+Owl drew the counting process as a pipeline:
106106+107107+```
108108+Check not empty → Count votes → Verify total → Find winner
109109+ ↓ ↓ ↓ ↓
110110+ Err? (ok) Err? Err?
111111+```
112112+113113+"Each step can fail. And unlike validation, if *any* step fails, we must stop."
114114+115115+```typescript
116116+const ensureNotEmpty = (
117117+ ballots: readonly Ballot[]
118118+): Result<readonly Ballot[], CountError> =>
119119+ ballots.length === 0
120120+ ? err({ _tag: "EmptyBallotBox" })
121121+ : ok(ballots)
122122+```
123123+124124+"First gate: are there ballots at all?"
125125+126126+---
127127+128128+```typescript
129129+const verifyTotal = (
130130+ ballots: readonly Ballot[],
131131+ counts: VoteCounts
132132+): Result<VoteCounts, CountError> => {
133133+ const summed = counts.Deer + counts.Squirrel + counts.Heron
134134+ return summed === ballots.length
135135+ ? ok(counts)
136136+ : err({
137137+ _tag: "TotalMismatch",
138138+ expected: ballots.length,
139139+ actual: summed,
140140+ })
141141+}
142142+```
143143+144144+"Second gate: does the sum match? If someone miscounted, or a ballot got lost, this will catch it."
145145+146146+Rabbit looked relieved. "So we'll know immediately if something's wrong."
147147+148148+"That's the point. With `Result`, we don't continue with bad data."
149149+150150+---
151151+152152+"What about finding the winner?" asked Beaver.
153153+154154+"That can also fail — if there's a tie."
155155+156156+```typescript
157157+const findWinner = (
158158+ counts: VoteCounts
159159+): Result<{ winner: Candidate; votes: number }, CountError> => {
160160+ const entries: readonly [Candidate, number][] = [
161161+ ["Deer", counts.Deer],
162162+ ["Squirrel", counts.Squirrel],
163163+ ["Heron", counts.Heron],
164164+ ]
165165+166166+ const maxVotes = Math.max(...entries.map(([, v]) => v))
167167+ const winners = entries.filter(([, v]) => v === maxVotes)
168168+169169+ return winners.length > 1
170170+ ? err({
171171+ _tag: "TieDetected",
172172+ candidates: winners.map(([c]) => c),
173173+ votes: maxVotes,
174174+ })
175175+ : ok({ winner: winners[0]![0], votes: maxVotes })
176176+}
177177+```
178178+179179+Fox leaned forward. "What happens in a tie? We can't just pick one."
180180+181181+"We fail with `TieDetected`. The forest will need a runoff or some other procedure. That's not our decision to make — we just report that we can't determine a single winner."
182182+183183+---
184184+185185+"Now," said Owl, "we chain them together. This is where `Result` shows its nature."
186186+187187+```typescript
188188+const runElection = (
189189+ ballots: readonly Ballot[]
190190+): Result<{ winner: Candidate; votes: number; total: number }, CountError> =>
191191+ pipe(
192192+ ensureNotEmpty(ballots),
193193+ chainResult((validBallots) => {
194194+ const counts = countVotes(validBallots)
195195+ return pipe(
196196+ verifyTotal(validBallots, counts),
197197+ chainResult((verifiedCounts) =>
198198+ pipe(
199199+ findWinner(verifiedCounts),
200200+ mapResult((result) => ({
201201+ ...result,
202202+ total: validBallots.length,
203203+ }))
204204+ )
205205+ )
206206+ )
207207+ })
208208+ )
209209+```
210210+211211+"Each `chainResult` says: if the previous step succeeded, do this next. If it failed, stop here and propagate the error."
212212+213213+---
214214+215215+Beaver traced through the code. "So if `ensureNotEmpty` fails..."
216216+217217+"We never count votes."
218218+219219+"And if `verifyTotal` fails..."
220220+221221+"We never look for a winner. The error from `verifyTotal` becomes the final result."
222222+223223+"That's very different from yesterday," Rabbit said. "Yesterday we wanted *all* the errors."
224224+225225+"Different problems, different tools." Owl nodded. "Validation is for independent checks — each field can be wrong in its own way. Result is for dependent operations — each step needs the previous one to succeed."
226226+227227+---
228228+229229+Fox had been thinking. "But what if I *want* to recover from an error? What if an empty ballot box just means 'no votes cast' rather than a failure?"
230230+231231+"Good question." Owl added another leaf.
232232+233233+```typescript
234234+const runElectionWithDefault = (
235235+ ballots: readonly Ballot[]
236236+): Result<{ winner: Candidate | null; votes: number; total: number }, CountError> =>
237237+ pipe(
238238+ runElection(ballots),
239239+ // Recover from EmptyBallotBox specifically
240240+ (result) =>
241241+ result._tag === "Err" && result.error._tag === "EmptyBallotBox"
242242+ ? ok({ winner: null, votes: 0, total: 0 })
243243+ : result
244244+ )
245245+```
246246+247247+"You can catch specific errors and provide defaults. Other errors still propagate."
248248+249249+---
250250+251251+"Let me show you why this matters," said Owl. "Watch what happens with bad data."
252252+253253+She wrote out test cases:
254254+255255+```typescript
256256+const formatError = (error: CountError): string => {
257257+ switch (error._tag) {
258258+ case "EmptyBallotBox":
259259+ return "No ballots in the box"
260260+ case "TotalMismatch":
261261+ return `Count mismatch: expected ${error.expected}, got ${error.actual}`
262262+ case "TieDetected":
263263+ return `Tie between ${error.candidates.join(" and ")} with ${error.votes} votes each`
264264+ }
265265+}
266266+267267+// Test 1: Normal election
268268+const normalBallots: readonly Ballot[] = [
269269+ { voter: "Rabbit", candidate: "Deer" },
270270+ { voter: "Fox", candidate: "Deer" },
271271+ { voter: "Beaver", candidate: "Squirrel" },
272272+ { voter: "Badger", candidate: "Deer" },
273273+ { voter: "Owl", candidate: "Heron" },
274274+]
275275+276276+matchResult(
277277+ (result) => console.log(`Winner: ${result.winner} with ${result.votes}/${result.total} votes`),
278278+ (error) => console.log(`Error: ${formatError(error)}`)
279279+)(runElection(normalBallots))
280280+// Winner: Deer with 3/5 votes
281281+```
282282+283283+"The happy path works as expected."
284284+285285+---
286286+287287+```typescript
288288+// Test 2: Empty box
289289+matchResult(
290290+ (result) => console.log(`Winner: ${result.winner}`),
291291+ (error) => console.log(`Error: ${formatError(error)}`)
292292+)(runElection([]))
293293+// Error: No ballots in the box
294294+```
295295+296296+"With an empty box, we stop immediately. No counting, no winner-finding."
297297+298298+```typescript
299299+// Test 3: Tie
300300+const tieBallots: readonly Ballot[] = [
301301+ { voter: "Rabbit", candidate: "Deer" },
302302+ { voter: "Fox", candidate: "Squirrel" },
303303+ { voter: "Beaver", candidate: "Deer" },
304304+ { voter: "Badger", candidate: "Squirrel" },
305305+]
306306+307307+matchResult(
308308+ (result) => console.log(`Winner: ${result.winner}`),
309309+ (error) => console.log(`Error: ${formatError(error)}`)
310310+)(runElection(tieBallots))
311311+// Error: Tie between Deer and Squirrel with 2 votes each
312312+```
313313+314314+"A tie is detected and reported. We don't pretend there's a winner when there isn't."
315315+316316+---
317317+318318+The sun had climbed higher. The real ballots still waited in their pile.
319319+320320+"I think I understand," said Beaver. "Validation is for checking everything independently. Result is for steps that depend on each other."
321321+322322+"Yesterday: find ALL the problems with a ballot."
323323+324324+"Today: stop at the FIRST problem with the count."
325325+326326+Owl nodded approvingly. "Two tools, two purposes. Use the right one for the job."
327327+328328+---
329329+330330+Badger cleared his throat. "Shall we count the real ballots, then?"
331331+332332+With Owl's framework in place, Beaver sorted while Rabbit counted. The pile totals matched. No ties emerged.
333333+334334+"Deer," announced Badger, "with twelve votes."
335335+336336+A murmur of approval rippled through the clearing. Deer dipped their head gracefully.
337337+338338+"Now we announce," said Badger. "And we must tell every corner of the forest."
339339+340340+"The messenger birds," said Rabbit, her worry returning. "They're not always reliable."
341341+342342+Owl began gathering fresh leaves. "Then we'll need something more than Result. We'll need something that can handle time, failure, and retry."
343343+344344+"Tomorrow," said Badger. "Today, we celebrate a successful count."
345345+346346+But Owl was already thinking ahead. Some problems couldn't be solved with pure functions. Some problems needed *effects*.
347347+348348+---
349349+350350+## What We Learned
351351+352352+1. **Result short-circuits on the first error** — Unlike Validation which collects all errors, Result stops immediately when something goes wrong.
353353+354354+2. **Use `chainResult` to sequence dependent operations** — Each step runs only if the previous succeeded.
355355+356356+3. **Use `mapResult` to transform success values** — Like adding the total to our winner result.
357357+358358+4. **Errors propagate automatically** — Once something fails, subsequent `chainResult` calls are skipped.
359359+360360+5. **Different tools for different problems** — Validation for independent checks, Result for dependent operations.
361361+362362+---
363363+364364+## Try It Yourself
365365+366366+The complete code from this story is available as a runnable example:
367367+368368+```bash
369369+bun examples/stories/forest-election/02-result.ts
370370+```
371371+372372+Try adding a corrupted count scenario where the totals don't match.
373373+374374+---
375375+376376+*Next: [The Announcement](./03-the-announcement.md)*
···11+# The Announcement
22+33+*Part 3 of The Forest Election*
44+55+> In which the animals learn that some things take time,
66+> can fail, and need to be tried again.
77+88+---
99+1010+The celebration had quieted. Deer stood gracefully beneath the oak, the new president of the forest. But the work wasn't done.
1111+1212+"Every creature must know," said Badger. "The North Meadow, the East Stream, the South Thicket, the Western Pines. News must reach them all."
1313+1414+"The messenger birds," said Rabbit. Her worry had returned. "Remember last winter? Half the messages never arrived."
1515+1616+Fox nodded slowly. "Sparrow got distracted by seeds. Jay argued with a squirrel and forgot her message entirely. And Wren... Wren just got lost."
1717+1818+"We can't just send messages and hope," said Beaver. "We need to know if they arrived."
1919+2020+Owl had been listening. "This is a different kind of problem. Validation checked things instantly. Counting was sequential. But sending messages... that takes *time*. And things that take time can fail in ways that instant operations can't."
2121+2222+---
2323+2424+"Let me show you what I mean," said Owl. "With our validation and counting, we wrote functions that returned immediately. But a message to the North Meadow doesn't return immediately. The bird has to fly there, deliver the message, and fly back."
2525+2626+```typescript
2727+import {
2828+ type Eff,
2929+ succeed,
3030+ fail,
3131+ sleep,
3232+ flatMap,
3333+ mapEff,
3434+ foldEff,
3535+ pipe,
3636+ runPromise,
3737+ race,
3838+ retry,
3939+ all,
4040+ fork,
4141+ join,
4242+} from "purus-ts"
4343+```
4444+4545+"These are *effects*. Unlike `Result` which is just data, an `Eff` is a description of something that will happen — later, when we run it."
4646+4747+---
4848+4949+"First, let's model our messengers and destinations."
5050+5151+```typescript
5252+type Region = "North Meadow" | "East Stream" | "South Thicket" | "Western Pines"
5353+5454+type MessageError =
5555+ | { readonly _tag: "BirdDistracted"; readonly bird: string; readonly reason: string }
5656+ | { readonly _tag: "BirdLost"; readonly bird: string }
5757+ | { readonly _tag: "Timeout"; readonly region: Region }
5858+5959+type DeliveryResult = {
6060+ readonly region: Region
6161+ readonly deliveredBy: string
6262+ readonly confirmationTime: number
6363+}
6464+```
6565+6666+"A message can fail because the bird got distracted, got lost, or took too long."
6767+6868+---
6969+7070+Rabbit studied the types. "But how do we represent the actual sending?"
7171+7272+"With an effect that describes what might happen." Owl scratched carefully.
7373+7474+```typescript
7575+const sendMessage = (
7676+ region: Region,
7777+ bird: string,
7878+ reliabilityPercent: number
7979+): Eff<DeliveryResult, MessageError, unknown> =>
8080+ pipe(
8181+ // Simulate flight time (200-800ms)
8282+ sleep(200 + Math.random() * 600),
8383+ flatMap(() => {
8484+ const roll = Math.random() * 100
8585+8686+ // Bird gets distracted
8787+ if (roll > reliabilityPercent) {
8888+ const distractions = ["seeds", "a shiny object", "another bird", "a warm breeze"]
8989+ return fail({
9090+ _tag: "BirdDistracted",
9191+ bird,
9292+ reason: distractions[Math.floor(Math.random() * distractions.length)]!,
9393+ })
9494+ }
9595+9696+ // Bird gets lost (5% chance even for reliable birds)
9797+ if (roll > reliabilityPercent - 5) {
9898+ return fail({ _tag: "BirdLost", bird })
9999+ }
100100+101101+ // Success!
102102+ return succeed({
103103+ region,
104104+ deliveredBy: bird,
105105+ confirmationTime: Date.now(),
106106+ })
107107+ })
108108+ )
109109+```
110110+111111+"This doesn't send anything yet," Owl emphasized. "It's just a *description* of sending. Nothing happens until we run it."
112112+113113+---
114114+115115+"That seems odd," said Fox. "What's the point of describing instead of doing?"
116116+117117+"Watch." Owl drew a diagram in the dirt.
118118+119119+```
120120+Promise (eager): Effect (lazy):
121121+122122+ const p = fetch(url) const e = sendMessage(...)
123123+ // Already started! // Nothing happened yet
124124+125125+ await p await runPromise(e)
126126+ // Just waiting // NOW it runs
127127+```
128128+129129+"With a Promise, the moment you create it, the work begins. With an Effect, creation and execution are separate. That means we can *compose* effects before running them."
130130+131131+---
132132+133133+Beaver's eyes lit up. "So we could build up a complex plan, then run it all at once?"
134134+135135+"Exactly. And we can add things like timeout, retry, and running multiple effects at the same time — all before anything actually executes."
136136+137137+```typescript
138138+// Add a timeout to any effect
139139+const sendWithTimeout = (
140140+ region: Region,
141141+ bird: string,
142142+ reliability: number,
143143+ timeoutMs: number
144144+): Eff<DeliveryResult | null, MessageError, unknown> =>
145145+ pipe(
146146+ race(
147147+ sendMessage(region, bird, reliability),
148148+ pipe(
149149+ sleep(timeoutMs),
150150+ mapEff((): DeliveryResult | null => null)
151151+ )
152152+ ),
153153+ flatMap((result) =>
154154+ result === null
155155+ ? fail({ _tag: "Timeout", region } as MessageError)
156156+ : succeed(result)
157157+ )
158158+ )
159159+```
160160+161161+"This sends a message but gives up if it takes too long. The `race` runs both effects and takes whichever finishes first."
162162+163163+---
164164+165165+"But if a bird fails," said Rabbit, "shouldn't we try again?"
166166+167167+"We can." Owl added another layer.
168168+169169+```typescript
170170+const sendWithRetry = (
171171+ region: Region,
172172+ bird: string,
173173+ reliability: number,
174174+ maxAttempts: number
175175+): Eff<DeliveryResult, MessageError, unknown> =>
176176+ pipe(
177177+ sendMessage(region, bird, reliability),
178178+ retry(maxAttempts - 1) // retry N-1 times = N total attempts
179179+ )
180180+```
181181+182182+"This tries the message multiple times. If the first attempt fails, it tries again, up to `maxAttempts` total."
183183+184184+---
185185+186186+"Now here's where it gets interesting," said Owl. "We have four regions to notify. We could send them one at a time..."
187187+188188+```typescript
189189+// Sequential: slow but simple
190190+const announceSequentially = (
191191+ message: string
192192+): Eff<DeliveryResult[], MessageError, unknown> =>
193193+ pipe(
194194+ sendWithRetry("North Meadow", "Sparrow", 70, 3),
195195+ flatMap((north) =>
196196+ pipe(
197197+ sendWithRetry("East Stream", "Jay", 80, 3),
198198+ flatMap((east) =>
199199+ pipe(
200200+ sendWithRetry("South Thicket", "Wren", 60, 3),
201201+ flatMap((south) =>
202202+ pipe(
203203+ sendWithRetry("Western Pines", "Robin", 85, 3),
204204+ mapEff((west) => [north, east, south, west])
205205+ )
206206+ )
207207+ )
208208+ )
209209+ )
210210+ )
211211+ )
212212+```
213213+214214+"But that's slow. Each bird waits for the previous one to return."
215215+216216+---
217217+218218+"We can send them all at once." Owl's eyes gleamed.
219219+220220+```typescript
221221+const announceInParallel = (): Eff<readonly DeliveryResult[], MessageError, unknown> =>
222222+ all([
223223+ sendWithRetry("North Meadow", "Sparrow", 70, 3),
224224+ sendWithRetry("East Stream", "Jay", 80, 3),
225225+ sendWithRetry("South Thicket", "Wren", 60, 3),
226226+ sendWithRetry("Western Pines", "Robin", 85, 3),
227227+ ])
228228+```
229229+230230+"`all` runs every effect at the same time. All four birds fly out together."
231231+232232+"What if one fails?" asked Fox.
233233+234234+"Then `all` fails with that error. If you want to collect all results regardless of failure, you need to handle errors inside each effect."
235235+236236+---
237237+238238+"Let me show you a more resilient version," said Owl.
239239+240240+```typescript
241241+type AnnouncementResult =
242242+ | { readonly _tag: "Delivered"; readonly result: DeliveryResult }
243243+ | { readonly _tag: "Failed"; readonly region: Region; readonly error: MessageError }
244244+245245+const sendAndCapture = (
246246+ region: Region,
247247+ bird: string,
248248+ reliability: number
249249+): Eff<AnnouncementResult, never, unknown> =>
250250+ pipe(
251251+ sendWithRetry(region, bird, reliability, 3),
252252+ foldEff(
253253+ (error) => succeed({ _tag: "Failed" as const, region, error }),
254254+ (result) => succeed({ _tag: "Delivered" as const, result })
255255+ )
256256+ )
257257+```
258258+259259+"`foldEff` handles both success and failure, converting failures into successful results that describe the failure. Now `all` will never fail — we collect all outcomes."
260260+261261+---
262262+263263+```typescript
264264+const announceToAllRegions = (): Eff<readonly AnnouncementResult[], never, unknown> =>
265265+ all([
266266+ sendAndCapture("North Meadow", "Sparrow", 70),
267267+ sendAndCapture("East Stream", "Jay", 80),
268268+ sendAndCapture("South Thicket", "Wren", 60),
269269+ sendAndCapture("Western Pines", "Robin", 85),
270270+ ])
271271+```
272272+273273+Rabbit traced through the logic. "So even if Wren fails, we still hear back from everyone else?"
274274+275275+"Exactly. We get a complete picture: who succeeded, who failed, and why."
276276+277277+---
278278+279279+"But wait," said Beaver. "You said nothing runs until we call `runPromise`. When do we actually send the birds?"
280280+281281+"Only when we explicitly run the effect." Owl demonstrated.
282282+283283+```typescript
284284+const main = async () => {
285285+ console.log("Sending announcement to all regions...")
286286+287287+ const results = await runPromise(announceToAllRegions())
288288+289289+ const delivered = results.filter((r): r is { _tag: "Delivered"; result: DeliveryResult } =>
290290+ r._tag === "Delivered"
291291+ )
292292+ const failed = results.filter((r): r is { _tag: "Failed"; region: Region; error: MessageError } =>
293293+ r._tag === "Failed"
294294+ )
295295+296296+ console.log(`Delivered: ${delivered.length}/4`)
297297+ delivered.forEach((d) =>
298298+ console.log(` ✓ ${d.result.region} via ${d.result.deliveredBy}`)
299299+ )
300300+301301+ console.log(`Failed: ${failed.length}/4`)
302302+ failed.forEach((f) =>
303303+ console.log(` ✗ ${f.region}: ${formatError(f.error)}`)
304304+ )
305305+}
306306+```
307307+308308+"The moment `runPromise` executes, all four birds take flight. The results come back as they complete."
309309+310310+---
311311+312312+Fox had been quiet, processing. "So the effect is like... a blueprint?"
313313+314314+"Yes." Owl nodded. "A Promise is a running computation. An Effect is a plan for computation. You can inspect it, transform it, combine it with others — all before anything happens."
315315+316316+"And retry, timeout, race — those are just transformations on the blueprint?"
317317+318318+"Exactly. `retry(3)` doesn't retry anything. It takes one blueprint and returns a new blueprint that says 'try this, and if it fails, try again up to 3 times.'"
319319+320320+---
321321+322322+The afternoon light was golden now. The messenger birds waited on their branches, ready but not yet launched.
323323+324324+"I understand now," said Rabbit. "Validation for independent checks. Result for dependent steps. Effects for things that take time and might fail."
325325+326326+"And effects compose," added Beaver. "Timeout, retry, parallel — we build them up piece by piece."
327327+328328+"Three tools for three kinds of problems," said Badger. "The forest is fortunate to have such careful builders."
329329+330330+---
331331+332332+Owl looked at the waiting birds. "Shall we send the announcement?"
333333+334334+Badger nodded.
335335+336336+Owl scratched one final mark — `runPromise(announceToAllRegions())` — and the birds took flight. Four shapes against the evening sky, carrying news to every corner of the forest.
337337+338338+Some would arrive quickly. Some might need a second try. But the plan was sound, the errors were handled, and the forest would know its new president.
339339+340340+*In the clearing beneath the ancient oak, the animals watched the birds disappear into the distance. The election was complete — validated, counted, and now announced. Deer stepped forward, ready to serve.*
341341+342342+*And somewhere in the machinery of the forest, effects were running, fibers were racing, and promises were being kept.*
343343+344344+---
345345+346346+## What We Learned
347347+348348+1. **Effects are descriptions, not actions** — An `Eff<A, E, R>` describes what might happen. Nothing runs until `runPromise`.
349349+350350+2. **Lazy evaluation enables composition** — Because effects are just data, we can transform them (retry, timeout, race) before running.
351351+352352+3. **`flatMap` chains sequential effects** — "Do this, then do that with the result."
353353+354354+4. **`all` runs effects in parallel** — All effects start at once; fails fast on first error.
355355+356356+5. **`foldEff` handles success and failure** — Convert failures to successes when you want to collect all results.
357357+358358+6. **`race` takes the first to complete** — Perfect for timeouts: race your effect against a sleep.
359359+360360+7. **`retry` automatically retries failures** — Wrap any effect with retry logic without changing the original code.
361361+362362+---
363363+364364+## Try It Yourself
365365+366366+The complete code from this story is available as a runnable example:
367367+368368+```bash
369369+bun examples/stories/forest-election/03-effects.ts
370370+```
371371+372372+Try adjusting the bird reliability percentages and watch how retry changes the outcomes.
373373+374374+---
375375+376376+*The End of The Forest Election*
377377+378378+---
379379+380380+## What's Next?
381381+382382+You've completed the Forest Election trilogy! You've learned:
383383+384384+- **Part 1:** Validation accumulates all errors (independent checks)
385385+- **Part 2:** Result short-circuits on first error (dependent operations)
386386+- **Part 3:** Effects describe async work with composition (time and failure)
387387+388388+To dive deeper into any of these concepts, see:
389389+390390+- [Validation and Error Accumulation](../../concepts/03-validation-and-error-accumulation.md)
391391+- [Why Errors as Values?](../../concepts/01-errors-as-values.md)
392392+- [Effect Composition](../../concepts/06-effect-composition.md)
393393+- [Fiber Internals](../../concepts/07-fiber-internals.md)
+92
docs/guides/stories/forest-election/README.md
···11+# The Forest Election
22+33+A story in three parts about building a fair election system.
44+55+---
66+77+## The Story
88+99+Every five years, the forest elects a president. This year, the animals decide to build proper software instead of trusting leaves in a box. Along the way, they discover why functional programming makes systems more reliable.
1010+1111+---
1212+1313+## Characters
1414+1515+| Character | Personality | Role in the Code |
1616+|-----------|-------------|------------------|
1717+| **Owl** | Methodical, patient | Introduces types and validation |
1818+| **Beaver** | Eager, practical | Learns why validation-first matters |
1919+| **Fox** | Skeptical, pragmatic | Questions complexity, comes around when the simple way fails |
2020+| **Rabbit** | Anxious, detail-oriented | Her worries become the error types |
2121+| **Badger** | Wise, experienced | Remembers past disasters, provides motivation |
2222+2323+**Candidates:** Deer (calm, diplomatic), Squirrel (energetic, populist), Heron (quiet wisdom).
2424+2525+---
2626+2727+## Parts
2828+2929+### Part 1: [The Ballot Box Problem](./01-the-ballot-box-problem.md)
3030+3131+The animals gather to elect a president, but the old way — leaves in a box — leads to chaos. Blank ballots, invalid candidates, duplicate votes. Fox wants to throw out bad ballots; Rabbit insists they need to know *all* the problems. Owl introduces the Validation type.
3232+3333+**Concept:** Validation and error accumulation
3434+3535+**You'll learn:**
3636+- Why `Result` short-circuits but `Validation` accumulates
3737+- Building validators with `valid` and `invalidOne`
3838+- Combining validators with `apValidation`
3939+- Using `matchValidation` to handle outcomes
4040+4141+---
4242+4343+### Part 2: [Counting Day](./02-counting-day.md)
4444+4545+The ballots are validated, but counting brings new challenges. What if the count doesn't match? What if there's a tie? The animals discover that some operations must short-circuit — and `Result` is the right tool.
4646+4747+**Concept:** Result and short-circuit error handling
4848+4949+**You'll learn:**
5050+- Why `Result` stops at the first error (unlike `Validation`)
5151+- Chaining dependent operations with `chainResult`
5252+- Transforming success values with `mapResult`
5353+- When to use Result vs Validation
5454+5555+---
5656+5757+### Part 3: [The Announcement](./03-the-announcement.md)
5858+5959+The winner must be announced to every corner of the forest. But the messenger birds are unreliable — some might not return. The animals need effects that can be retried, raced, and cancelled.
6060+6161+**Concept:** Effects, fibers, and concurrency
6262+6363+**You'll learn:**
6464+- Effects as descriptions vs execution
6565+- Chaining async operations with `flatMap`
6666+- Running effects in parallel with `all`
6767+- Error recovery with `foldEff`
6868+- Automatic retry with `retry`
6969+7070+---
7171+7272+## Running the Code
7373+7474+Each part has a matching example file:
7575+7676+```bash
7777+# Part 1: Validation
7878+bun examples/stories/forest-election/01-validation.ts
7979+8080+# Part 2: Result
8181+bun examples/stories/forest-election/02-result.ts
8282+8383+# Part 3: Effects
8484+bun examples/stories/forest-election/03-effects.ts
8585+```
8686+8787+---
8888+8989+## See Also
9090+9191+- [Stories overview](../) — Other available stories
9292+- [Validation and Error Accumulation](../../concepts/03-validation-and-error-accumulation.md) — Technical deep-dive
+206
examples/stories/forest-election/01-validation.ts
···11+/**
22+ * The Forest Election — Part 1: Validation
33+ * ==========================================
44+ *
55+ * This example accompanies the story at:
66+ * docs/guides/stories/forest-election/01-the-ballot-box-problem.md
77+ *
88+ * It demonstrates how Validation accumulates ALL errors rather than
99+ * stopping at the first one — perfect for form validation, ballot
1010+ * checking, or any scenario where you want complete feedback.
1111+ *
1212+ * Run with: bun examples/stories/forest-election/01-validation.ts
1313+ */
1414+1515+import {
1616+ type Validation,
1717+ valid,
1818+ invalidOne,
1919+ apValidation,
2020+ matchValidation,
2121+ pipe,
2222+} from "../../../src/index"
2323+2424+// =============================================================================
2525+// Domain Types
2626+// =============================================================================
2727+2828+type Candidate = "Deer" | "Squirrel" | "Heron"
2929+3030+type Ballot = {
3131+ readonly voter: string
3232+ readonly candidate: Candidate
3333+}
3434+3535+type BallotError =
3636+ | { readonly _tag: "MissingVoter" }
3737+ | { readonly _tag: "MissingCandidate" }
3838+ | { readonly _tag: "InvalidCandidate"; readonly name: string }
3939+ | { readonly _tag: "DuplicateVoter"; readonly voter: string }
4040+4141+// =============================================================================
4242+// Validators
4343+// =============================================================================
4444+4545+const VALID_CANDIDATES: readonly Candidate[] = ["Deer", "Squirrel", "Heron"]
4646+4747+/**
4848+ * Validate that a voter name is present and non-empty.
4949+ */
5050+const validateVoter = (
5151+ voter: string | undefined,
5252+): Validation<string, BallotError> =>
5353+ voter && voter.trim().length > 0
5454+ ? valid(voter.trim())
5555+ : invalidOne({ _tag: "MissingVoter" })
5656+5757+/**
5858+ * Validate that a candidate is present and one of the valid options.
5959+ */
6060+const validateCandidate = (
6161+ candidate: string | undefined,
6262+): Validation<Candidate, BallotError> =>
6363+ !candidate || candidate.trim().length === 0
6464+ ? invalidOne({ _tag: "MissingCandidate" })
6565+ : VALID_CANDIDATES.includes(candidate as Candidate)
6666+ ? valid(candidate as Candidate)
6767+ : invalidOne({ _tag: "InvalidCandidate", name: candidate })
6868+6969+/**
7070+ * Validate that a voter hasn't already voted.
7171+ */
7272+const validateNoDuplicate = (
7373+ voter: string,
7474+ alreadyVoted: ReadonlySet<string>,
7575+): Validation<string, BallotError> =>
7676+ alreadyVoted.has(voter)
7777+ ? invalidOne({ _tag: "DuplicateVoter", voter })
7878+ : valid(voter)
7979+8080+// =============================================================================
8181+// Combined Validator
8282+// =============================================================================
8383+8484+/**
8585+ * Curried ballot constructor for use with apValidation.
8686+ */
8787+const makeBallot =
8888+ (voter: string) =>
8989+ (candidate: Candidate): Ballot => ({
9090+ voter,
9191+ candidate,
9292+ })
9393+9494+/**
9595+ * Validate a complete ballot, checking:
9696+ * - Voter is present
9797+ * - Voter hasn't voted before
9898+ * - Candidate is present and valid
9999+ *
100100+ * All errors are accumulated — you get the complete list of problems.
101101+ */
102102+const validateBallot = (
103103+ voter: string | undefined,
104104+ candidate: string | undefined,
105105+ alreadyVoted: ReadonlySet<string>,
106106+): Validation<Ballot, BallotError> =>
107107+ pipe(
108108+ valid(makeBallot),
109109+ apValidation(
110110+ pipe(validateVoter(voter), (voterResult) =>
111111+ voterResult._tag === "Valid"
112112+ ? pipe(
113113+ validateNoDuplicate(voterResult.value, alreadyVoted),
114114+ (dupResult) => (dupResult._tag === "Valid" ? voterResult : dupResult),
115115+ )
116116+ : voterResult,
117117+ ),
118118+ ),
119119+ apValidation(validateCandidate(candidate)),
120120+ )
121121+122122+// =============================================================================
123123+// Error Formatting
124124+// =============================================================================
125125+126126+const formatError = (error: BallotError): string => {
127127+ switch (error._tag) {
128128+ case "MissingVoter":
129129+ return "Ballot is missing a voter name"
130130+ case "MissingCandidate":
131131+ return "Ballot is missing a candidate"
132132+ case "InvalidCandidate":
133133+ return `"${error.name}" is not a valid candidate`
134134+ case "DuplicateVoter":
135135+ return `${error.voter} has already voted`
136136+ }
137137+}
138138+139139+// =============================================================================
140140+// Demo
141141+// =============================================================================
142142+143143+console.log("=== The Forest Election: Ballot Validation ===\n")
144144+145145+const testBallots: Array<{
146146+ voter: string | undefined
147147+ candidate: string | undefined
148148+}> = [
149149+ { voter: "Rabbit", candidate: "Deer" }, // Valid
150150+ { voter: undefined, candidate: "Squirrel" }, // Missing voter
151151+ { voter: "Fox", candidate: undefined }, // Missing candidate
152152+ { voter: "Beaver", candidate: "Wolfe" }, // Invalid candidate
153153+ { voter: undefined, candidate: undefined }, // Both missing!
154154+ { voter: "Rabbit", candidate: "Heron" }, // Duplicate voter
155155+ { voter: "Badger", candidate: "Deer" }, // Valid
156156+ { voter: " ", candidate: "Squirrel" }, // Blank voter (whitespace)
157157+ { voter: "Owl", candidate: "Eagle" }, // Invalid candidate
158158+]
159159+160160+const alreadyVoted = new Set<string>()
161161+const validBallots: Ballot[] = []
162162+const invalidBallots: Array<{
163163+ index: number
164164+ errors: readonly BallotError[]
165165+}> = []
166166+167167+console.log("Processing ballots...\n")
168168+169169+testBallots.forEach(({ voter, candidate }, i) => {
170170+ const result = validateBallot(voter, candidate, alreadyVoted)
171171+172172+ matchValidation(
173173+ (ballot: Ballot) => {
174174+ console.log(
175175+ `Ballot ${i + 1}: Valid - ${ballot.voter} votes for ${ballot.candidate}`,
176176+ )
177177+ alreadyVoted.add(ballot.voter)
178178+ validBallots.push(ballot)
179179+ },
180180+ (errors: readonly BallotError[]) => {
181181+ console.log(`Ballot ${i + 1}: Invalid`)
182182+ errors.forEach((e) => console.log(` - ${formatError(e)}`))
183183+ invalidBallots.push({ index: i + 1, errors })
184184+ },
185185+ )(result)
186186+})
187187+188188+// Summary
189189+console.log("\n" + "=".repeat(50))
190190+console.log("Summary")
191191+console.log("=".repeat(50))
192192+console.log(`Valid ballots: ${validBallots.length}`)
193193+console.log(`Invalid ballots: ${invalidBallots.length}`)
194194+195195+console.log("\nVoters who cast valid ballots:")
196196+validBallots.forEach((b) => console.log(` - ${b.voter} → ${b.candidate}`))
197197+198198+console.log("\nBallots with multiple errors:")
199199+invalidBallots
200200+ .filter((b) => b.errors.length > 1)
201201+ .forEach((b) => {
202202+ console.log(` Ballot ${b.index}: ${b.errors.length} errors`)
203203+ })
204204+205205+console.log("\n=== End of validation phase ===")
206206+console.log("Tomorrow, we count. And counting can also go wrong...")