An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update examples README for new structure

+29 -165
+29 -165
examples/README.md
··· 1 1 # purus-ts Examples 2 2 3 - Real-world examples demonstrating how purus-ts makes impossible states unrepresentable and complex async workflows simple. 3 + Learn purus-ts through side-by-side comparisons with vanilla TypeScript. 4 4 5 - ## Running Examples 6 - 7 - ```bash 8 - # Run any example 9 - bun run examples/http-client.ts 10 - bun run examples/workflow-engine.ts 11 - bun run examples/reactive-dom.ts 12 - bun run examples/parallel-scraper.ts 13 - bun run examples/task-queue.ts 14 - bun run examples/game-loop.ts 15 - ``` 16 - 17 - --- 18 - 19 - ## Examples Overview 20 - 21 - ### 1. [http-client.ts](./http-client.ts) - Resilient HTTP Client 22 - 23 - **Showcases:** Effect composition, retry, timeout, race, typed errors, dependency injection 24 - 25 - | Problem | Vanilla JS/TS | purus-ts | 26 - |---------|---------------|----------| 27 - | Error types | `catch (e: unknown)` | `Eff<Response, HttpError, Env>` | 28 - | Cancellation | Manual AbortController | Automatic on fiber interrupt | 29 - | Retry logic | Scattered try/catch | `pipe(request, retry(3))` | 30 - | Timeout | Promise.race + cleanup | `timeout(5000)` | 31 - | Fastest server | Race doesn't cancel losers | `race()` cancels losers | 32 - 33 - ```typescript 34 - // Compose timeout, retry, and error handling in one line 35 - const request = pipe( 36 - fetchWithAbort(url), 37 - timeout(5000), 38 - retry(3), 39 - catchAll(handleError) 40 - ) 41 - ``` 42 - 43 - --- 44 - 45 - ### 2. [workflow-engine.ts](./workflow-engine.ts) - Order Processing Pipeline 46 - 47 - **Showcases:** Branded types, refinements, typestate, Result, pattern matching 5 + Each example includes: 6 + - **README.md** - The problem and why purus helps 7 + - **without-purus.ts** - How most developers write it (realistic, not strawman) 8 + - **with-purus.ts** - The purus solution 48 9 49 - | Problem | Vanilla JS/TS | purus-ts | 50 - |--------------------|--------------------------------------------------|---------------------------------------------------------------------| 51 - | ID mixups | `processOrder(orderId, userId)` - args swappable | `processOrder(orderId: OrderId, userId: UserId)` - compiler catches | 52 - | Invalid quantities | `if (qty > 0)` checks everywhere | `PositiveInt` type - validated at construction | 53 - | State transitions | `if (order.status === 'paid')` | `ship(order: PaidOrder)` - can't ship unpaid | 54 - | Error handling | Switch with forgotten cases | `match()` forces exhaustive handling | 55 - 56 - ```typescript 57 - // Can't ship an unpaid order - compile error! 58 - ship(draftOrder) // Type 'DraftOrder' not assignable to 'PaidOrder' 59 - 60 - // All error cases must be handled 61 - match(error)({ 62 - OutOfStock: ..., // Required 63 - PaymentDeclined: ..., // Required 64 - InvalidAddress: ..., // Required - forget one = compile error 65 - }) 66 - ``` 67 - 68 - --- 69 - 70 - ### 3. [reactive-dom.ts](./reactive-dom.ts) - Reactive DOM with Cancellation 71 - 72 - **Showcases:** Fiber cancellation, async effects, resource cleanup, typestate 73 - 74 - | Problem | Vanilla JS/TS | purus-ts | 75 - |---------|---------------|----------| 76 - | Event listeners | Manual add/remove tracking | Cleanup function auto-called on interrupt | 77 - | Memory leaks | Easy to forget cleanup | Impossible - cleanup is part of effect | 78 - | Form validation | `if (form.isValid) submit()` | `submit(form: ValidForm)` - compile-time | 79 - | Debounce | Wrapper functions, manual timing | `pipe(input, debounce(300))` | 80 - 81 - ```typescript 82 - // Event listener cleanup is automatic 83 - const fromEvent = (target, type) => 84 - async((resume) => { 85 - const handler = (e) => resume(Exit.succeed(e)) 86 - target.addEventListener(type, handler) 87 - return () => target.removeEventListener(type, handler) // Auto-called! 88 - }) 89 - 90 - // Race submit vs cancel - loser is interrupted 91 - race(onSubmit, onCancel) 92 - ``` 93 - 94 - --- 95 - 96 - ### 4. [parallel-scraper.ts](./parallel-scraper.ts) - Concurrent Web Scraper 10 + ## Examples 97 11 98 - **Showcases:** Fork, all, race, timeout, retry, fiber supervision, graceful shutdown 12 + ### [http-client](./http-client/) 99 13 100 - | Problem | Vanilla JS/TS | purus-ts | 101 - |---------|---------------|----------| 102 - | Cancellation | Manual AbortController per request | Fiber interrupt propagates | 103 - | Concurrency limit | Complex semaphore implementation | Fork workers, coordinate with join | 104 - | Progress tracking | Callbacks, events | `fiber.status()` | 105 - | Shutdown | Track all promises, abort each | Interrupt parent → all children stop | 14 + **Problem:** Fetching data with retry and timeout shouldn't require 50 lines of boilerplate. 106 15 107 - ```typescript 108 - // Race mirrors - first success wins, others cancelled 109 - const fastest = race( 110 - fetch(mirror1), 111 - fetch(mirror2), 112 - fetch(mirror3) 113 - ) 16 + **Key concepts:** `pipe`, `retry`, `timeout`, typed errors, automatic cleanup 114 17 115 - // Graceful shutdown - one line 116 - pool.shutdown() // All workers interrupted, cleanup runs 18 + ```bash 19 + bun run examples/http-client/without-purus.ts 20 + bun run examples/http-client/with-purus.ts 117 21 ``` 118 22 119 23 --- 120 24 121 - ### 5. [task-queue.ts](./task-queue.ts) - Background Job Queue 25 + ### [workflow-engine](./workflow-engine/) 122 26 123 - **Showcases:** Fork/join, fiber supervision, retry, timeout, dependency injection 27 + **Problem:** Passing the wrong ID to a function shouldn't compile. Invalid state transitions shouldn't be possible. 124 28 125 - | Problem | Vanilla JS/TS | purus-ts | 126 - |---------|---------------|----------| 127 - | Worker management | Manual thread/process coordination | Workers are fibers | 128 - | Job retry | Per-job try/catch, counter management | `pipe(job, retry(3))` | 129 - | Timeout per job | Manual setTimeout + cleanup | `timeout(30000)` | 130 - | Dead letter queue | Separate error handling | `catchAll(e => pushToDLQ(job, e))` | 29 + **Key concepts:** Branded types, typestate, `match`, Result 131 30 132 - ```typescript 133 - // Type-safe job handling - add new job type = compiler shows where to handle 134 - const process = match(job)({ 135 - SendEmail: ..., // Required 136 - ProcessImage: ..., // Required 137 - GenerateReport: ..., // Required 138 - SyncData: ..., // Required 139 - Cleanup: ..., // Required - new job type = add here 140 - }) 31 + ```bash 32 + bun run examples/workflow-engine/without-purus.ts 33 + bun run examples/workflow-engine/with-purus.ts 141 34 ``` 142 35 143 36 --- 144 37 145 - ### 6. [game-loop.ts](./game-loop.ts) - Game Loop with Typed State 38 + ### [task-queue](./task-queue/) 146 39 147 - **Showcases:** Typestate, units, tracked arrays, effect scheduling 40 + **Problem:** Background job processing with retries and timeouts shouldn't require a framework. 148 41 149 - | Problem | Vanilla JS/TS | purus-ts | 150 - |---------|---------------|----------| 151 - | Dead entity attacks | `if (entity.state !== 'dead')` | `attack(entity: AliveEntity)` - compile error for dead | 152 - | Unit mixing | `position += velocity` - wrong! | `Position<Pixels> + Velocity<PixelsPerSecond>` - type error | 153 - | Unsorted render | Hope you sorted by z-index | `Arr<Entity, Sorted>` - guaranteed | 154 - | Frame timing | Manual requestAnimationFrame | `pipe(update, render, sleep(16), loop)` | 155 - 156 - ```typescript 157 - // Can't attack from Dead state - compile error! 158 - attack(deadEntity) // Type 'DeadEntity' not assignable to 'IdleEntity | MovingEntity' 42 + **Key concepts:** `fork`/`join`, `catchAll`, `provide` for DI 159 43 160 - // Can't mix position and velocity - compile error! 161 - addPixels(position, velocity) // Type 'PixelsPerSecond' not assignable to 'Pixels' 44 + ```bash 45 + bun run examples/task-queue/without-purus.ts 46 + bun run examples/task-queue/with-purus.ts 162 47 ``` 163 48 164 49 --- 165 50 166 - ## Key Takeaways 167 - 168 - ### Why purus-ts over vanilla TypeScript? 169 - 170 - 1. **Impossible states are unrepresentable** 171 - - Branded types prevent ID mixups 172 - - Refinements guarantee validated values 173 - - Typestate prevents invalid transitions 174 - - Pattern matching forces exhaustive handling 175 - 176 - 2. **Effects are data, not computation** 177 - - Can inspect, compose, transform effects before running 178 - - Cancellation propagates automatically 179 - - Stack-safe by construction 51 + ## Why Side-by-Side? 180 52 181 - 3. **Concurrency is first-class** 182 - - `fork`, `join`, `race`, `all` - composable primitives 183 - - Automatic cleanup on interrupt 184 - - No callback hell 53 + The best way to understand purus-ts is to see the same problem solved both ways: 185 54 186 - 4. **Dependency injection without frameworks** 187 - - `access<Env>()` reads environment 188 - - `provide(env)(effect)` injects dependencies 189 - - Pure functions, no decorators or reflection 55 + 1. **without-purus.ts** shows realistic vanilla code - not a strawman, but how most developers actually write it 56 + 2. **with-purus.ts** shows the purus solution - notice what disappears and what becomes explicit 190 57 191 - 5. **Composition over configuration** 192 - - `pipe(effect, timeout(5000), retry(3))` - one line 193 - - Each combinator is a pure function 194 - - Build complex workflows from simple parts 58 + Look for the `⚠️ PROBLEM:` comments in vanilla code and `✓ SOLUTION:` comments in purus code.