An educational pure functional programming library in TypeScript
2
fork

Configure Feed

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

Rewrite README with comprehensive documentation

+207 -6
+207 -6
README.md
··· 1 1 # purus-ts 2 2 3 - To install dependencies: 3 + Pure functional TypeScript with typed errors, fiber concurrency, and zero dependencies. 4 + 5 + > *"purus"* is Latin for *"pure"* 6 + 7 + ## Features 8 + 9 + - **Branded Types** — Create distinct types from primitives (`UserId`, `Email`) 10 + - **Refinements** — Encode validated properties in the type system (`Positive`, `NonEmpty`, `Sorted`) 11 + - **Result & Option** — Handle errors and nullability as values, not exceptions 12 + - **Pattern Matching** — Exhaustive, type-safe matching on discriminated unions 13 + - **Effect System** — Composable effects with typed errors and dependency injection 14 + - **Fiber Runtime** — Cooperative concurrency with cancellation, racing, and parallelism 15 + - **Zero Dependencies** — Single-file library, ~950 lines of TypeScript 16 + 17 + ## Installation 4 18 5 19 ```bash 6 - bun install 20 + npm install purus-ts 21 + # or 22 + bun add purus-ts 23 + # or 24 + pnpm add purus-ts 25 + ``` 26 + 27 + ## Quick Start 28 + 29 + ```typescript 30 + import { pipe, ok, err, mapResult, match, succeed, flatMap, runPromise } from 'purus-ts' 31 + 32 + // Result: errors as values 33 + const divide = (a: number, b: number) => 34 + b === 0 ? err('division by zero') : ok(a / b) 35 + 36 + const result = pipe( 37 + divide(10, 2), 38 + mapResult(x => x * 2) 39 + ) 40 + 41 + match(result)({ 42 + Ok: ({ value }) => console.log(`Result: ${value}`), 43 + Err: ({ error }) => console.log(`Error: ${error}`) 44 + }) 45 + 46 + // Effects: async operations with typed errors 47 + const fetchUser = pipe( 48 + succeed({ id: 1, name: 'Alice' }), 49 + flatMap(user => succeed(`Hello, ${user.name}!`)) 50 + ) 51 + 52 + runPromise(fetchUser).then(console.log) // "Hello, Alice!" 53 + ``` 54 + 55 + ## Core Concepts 56 + 57 + ### Branded Types 58 + 59 + Create distinct types that are incompatible at compile time: 60 + 61 + ```typescript 62 + import { brand, type Branded } from 'purus-ts' 63 + 64 + type UserId = Branded<string, 'UserId'> 65 + type OrderId = Branded<string, 'OrderId'> 66 + 67 + const userId: UserId = brand('user-123') 68 + const orderId: OrderId = brand('order-456') 69 + 70 + // userId = orderId // Compile error! 71 + ``` 72 + 73 + ### Refinements 74 + 75 + Encode validated properties in the type system: 76 + 77 + ```typescript 78 + import { positive, nonNegative, type Refined } from 'purus-ts' 79 + 80 + const price = positive(99.99) // Refined<number, 'Positive'> | undefined 81 + if (price) { 82 + // TypeScript knows price > 0 83 + } 84 + ``` 85 + 86 + ### Result & Option 87 + 88 + Handle errors and nullability without exceptions: 89 + 90 + ```typescript 91 + import { ok, err, some, none, mapResult, getOrElse, pipe } from 'purus-ts' 92 + 93 + // Result<T, E> = Ok<T> | Err<E> 94 + const parseNumber = (s: string) => { 95 + const n = Number(s) 96 + return isNaN(n) ? err('not a number') : ok(n) 97 + } 98 + 99 + // Option<T> = Some<T> | None 100 + const findUser = (id: string) => 101 + id === 'admin' ? some({ name: 'Admin' }) : none 102 + 103 + pipe(findUser('admin'), getOrElse({ name: 'Guest' })) 104 + ``` 105 + 106 + ### Pattern Matching 107 + 108 + Exhaustive matching on tagged unions: 109 + 110 + ```typescript 111 + import { match, when } from 'purus-ts' 112 + 113 + type Shape = 114 + | { _tag: 'Circle'; radius: number } 115 + | { _tag: 'Rectangle'; width: number; height: number } 116 + 117 + const area = (shape: Shape) => 118 + match(shape)({ 119 + Circle: ({ radius }) => Math.PI * radius ** 2, 120 + Rectangle: ({ width, height }) => width * height, 121 + }) 122 + 123 + // Predicate-based matching 124 + const grade = (score: number) => 125 + when(score)( 126 + [s => s >= 90, () => 'A'], 127 + [s => s >= 80, () => 'B'], 128 + )(() => 'F') 7 129 ``` 8 130 9 - To run: 131 + ### Effects & Fibers 132 + 133 + Composable async operations with typed errors: 134 + 135 + ```typescript 136 + import { 137 + succeed, fail, fromPromise, flatMap, catchAll, 138 + fork, join, race, all, timeout, retry, 139 + access, provide, runPromise, pipe 140 + } from 'purus-ts' 141 + 142 + // Dependency injection 143 + type Config = { apiUrl: string } 144 + 145 + const fetchData = pipe( 146 + access<Config, string>(env => env.apiUrl), 147 + flatMap(url => fromPromise(() => fetch(url).then(r => r.json()))) 148 + ) 10 149 11 - ```bash 12 - bun run 150 + // Provide dependencies at the edge 151 + runPromise(pipe(fetchData, provide({ apiUrl: 'https://api.example.com' }))) 152 + 153 + // Concurrency 154 + const fast = pipe(sleep(10), mapEff(() => 'fast')) 155 + const slow = pipe(sleep(100), mapEff(() => 'slow')) 156 + 157 + runPromise(race(fast, slow)) // 'fast' wins, slow is cancelled 158 + 159 + // Parallel execution 160 + runPromise(all([fetchA, fetchB, fetchC])) // Run in parallel, fail fast 13 161 ``` 14 162 15 - This project was created using `bun init` in bun v1.3.4. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. 163 + ## API Overview 164 + 165 + ### Foundations 166 + 167 + | Category | Exports | 168 + |----------|---------| 169 + | **Brands** | `Branded`, `brand` | 170 + | **Refinements** | `Refined`, `refine`, `positive`, `nonNegative`, `normalized`, `integer` | 171 + | **Result** | `Result`, `Ok`, `Err`, `ok`, `err`, `mapResult`, `chainResult`, `unwrapOr`, `tryCatch` | 172 + | **Option** | `Option`, `Some`, `None`, `some`, `none`, `fromNullable`, `mapOption`, `getOrElse`, `flatMapOption` | 173 + | **Pattern Matching** | `match`, `matchOr`, `when`, `matchLiteral`, `matchResult`, `matchOption` | 174 + | **Arrays** | `Arr`, `arr`, `sort`, `sortNum`, `sortBy`, `map`, `filter`, `head`, `last`, `binarySearch` | 175 + | **Units** | `Quantity`, `meters`, `seconds`, `velocity`, `addQ`, `scaleQ` | 176 + | **Typestate** | `Entity`, `entity`, `transition` | 177 + | **Composition** | `pipe`, `flow`, `id`, `constant`, `flip`, `tap` | 178 + | **Guards** | `Guard`, `and`, `or`, `isString`, `isNumber`, `isBoolean` | 179 + 180 + ### Effect System 181 + 182 + | Category | Exports | 183 + |----------|---------| 184 + | **Types** | `Eff`, `Fiber`, `FiberId`, `Exit`, `FiberStatus` | 185 + | **Constructors** | `succeed`, `fail`, `sync`, `attempt`, `async`, `fromPromise` | 186 + | **Transformations** | `mapEff`, `flatMap`, `foldEff`, `catchAll` | 187 + | **Dependencies** | `access`, `accessEff`, `provide` | 188 + | **Concurrency** | `fork`, `join`, `interruptFiber`, `race`, `all` | 189 + | **Combinators** | `sleep`, `timeout`, `retry`, `repeatEff`, `tapEff` | 190 + | **Runners** | `runFiber`, `runPromise`, `runPromiseExit`, `runPromiseWith` | 191 + 192 + ## Examples 193 + 194 + See the [`examples/`](./examples) directory for real-world usage: 195 + 196 + - **[http-client.ts](./examples/http-client.ts)** — Resilient HTTP client with typed errors, retries, and timeouts 197 + - **[workflow-engine.ts](./examples/workflow-engine.ts)** — Order processing pipeline with state machines 198 + - **[reactive-dom.ts](./examples/reactive-dom.ts)** — DOM event handling with cancellation 199 + - **[parallel-scraper.ts](./examples/parallel-scraper.ts)** — Concurrent web scraper with rate limiting 200 + - **[task-queue.ts](./examples/task-queue.ts)** — Background job processor with priorities 201 + - **[game-loop.ts](./examples/game-loop.ts)** — Game loop with typed units and state 202 + 203 + ## Philosophy 204 + 205 + purus-ts is an **educational library** demonstrating pure functional programming patterns in TypeScript: 206 + 207 + 1. **Errors as values** — Use `Result` and typed `Eff` errors instead of exceptions 208 + 2. **Effects as data** — Effects are instruction trees interpreted by the runtime 209 + 3. **Composition over inheritance** — Build complex behavior from simple functions via `pipe` and `flow` 210 + 4. **Types encode invariants** — Use phantom types to make illegal states unrepresentable 211 + 212 + Inspired by [Effect-TS](https://effect.website), [fp-ts](https://gcanti.github.io/fp-ts/), and Scala's ZIO. 213 + 214 + ## License 215 + 216 + [MIT](./LICENSE)