An educational pure functional programming library in TypeScript
2
fork

Configure Feed

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

Add JSDoc documentation to core exports

+772 -34
+232 -11
src/effect/eff.ts
··· 1 + /** 2 + * Effect type and core operations. 3 + * 4 + * Eff<A, E, R> is a lazy, composable description of an effectful computation. 5 + * Unlike Promises which start immediately, Effects are just data until you run them. 6 + * 7 + * @module effect/eff 8 + */ 9 + 1 10 import type { Exit } from "./exit" 2 11 3 12 /** 4 13 * Cleanup function returned by async effects. 14 + * Called when the effect is interrupted or times out. 5 15 */ 6 16 export type Cleanup = () => void 7 17 8 18 /** 9 - * Eff<A, E, R> - Free monad representing an effectful computation. 10 - * A = Success type, E = Error type, R = Required environment 19 + * Eff<A, E, R> - A lazy, composable, typed effect. 20 + * 21 + * Type parameters: 22 + * - A: Success type - what you get if the effect succeeds 23 + * - E: Error type - what you get if the effect fails 24 + * - R: Requirements - dependencies the effect needs to run 25 + * 26 + * Effects are pure data describing WHAT to do, not HOW to do it. 27 + * The runtime (runPromise, runFiber) handles execution. 28 + * 29 + * @example 30 + * ```typescript 31 + * // An effect that succeeds with a number 32 + * const eff1: Eff<number, never, unknown> = succeed(42) 33 + * 34 + * // An effect that fails with an error 35 + * const eff2: Eff<never, MyError, unknown> = fail({ _tag: "MyError" }) 36 + * 37 + * // An effect that requires a Logger 38 + * const eff3: Eff<void, never, Logger> = accessEff(logger => ...) 39 + * ``` 11 40 * 12 - * This is pure data describing WHAT to do, not HOW to do it. 13 - * The interpreter (trampoline) handles execution. 14 - */ 15 - /** 16 - * FiberId - unique identifier for a fiber 41 + * @template A - Success type 42 + * @template E - Error type 43 + * @template R - Required environment 17 44 */ 18 - export type FiberId = { readonly _tag: "FiberId"; readonly id: number } 19 - 20 45 export type Eff<A, E, R> = 21 46 | { readonly _tag: "Succeed"; readonly value: A } 22 47 | { readonly _tag: "Fail"; readonly error: E } ··· 31 56 | { readonly _tag: "YieldNow" } 32 57 | { readonly _tag: "GetFiberId" } 33 58 59 + /** 60 + * FiberId - unique identifier for a fiber. 61 + */ 62 + export type FiberId = { readonly _tag: "FiberId"; readonly id: number } 63 + 34 64 // === Constructors === 35 65 66 + /** 67 + * Create an effect that succeeds with a value. 68 + * 69 + * @example 70 + * ```typescript 71 + * const eff = succeed(42) 72 + * await runPromise(eff) // 42 73 + * ``` 74 + */ 36 75 export const succeed = <A>(value: A): Eff<A, never, unknown> => 37 76 ({ _tag: "Succeed", value }) 38 77 78 + /** 79 + * Create an effect that fails with an error. 80 + * 81 + * @example 82 + * ```typescript 83 + * const eff = fail({ _tag: "NotFound", id: "123" }) 84 + * await runPromise(eff) // throws { _tag: "NotFound", id: "123" } 85 + * ``` 86 + */ 39 87 export const fail = <E>(error: E): Eff<never, E, unknown> => 40 88 ({ _tag: "Fail", error }) 41 89 90 + /** 91 + * Create an effect from a synchronous function. 92 + * The function runs each time the effect is executed. 93 + * 94 + * @example 95 + * ```typescript 96 + * const now = sync(() => new Date()) 97 + * await runPromise(now) // Current date 98 + * await runPromise(now) // New current date 99 + * ``` 100 + */ 42 101 export const sync = <A>(f: () => A): Eff<A, never, unknown> => 43 102 ({ _tag: "Sync", f }) 44 103 ··· 55 114 * Use this to integrate external async APIs. For Promises, prefer `fromPromise`. 56 115 * 57 116 * @example 117 + * ```typescript 58 118 * // Wrap setTimeout 59 119 * const delay = (ms: number): Eff<void, never, unknown> => 60 120 * async(resume => { ··· 69 129 * el.addEventListener("click", handler) 70 130 * return () => el.removeEventListener("click", handler) 71 131 * }) 132 + * ``` 72 133 */ 73 134 export const async = <A, E>( 74 135 register: (resume: (exit: Exit<A, E>) => void) => Cleanup 75 136 ): Eff<A, E, unknown> => 76 137 ({ _tag: "Async", register }) 77 138 139 + /** 140 + * Yield control to other fibers. 141 + * Useful for cooperative multitasking in long-running computations. 142 + */ 78 143 export const yieldNow: Eff<void, never, unknown> = 79 144 ({ _tag: "YieldNow" }) 80 145 ··· 82 147 let fiberIdCounter = 0 83 148 export const makeFiberId = (): FiberId => ({ _tag: "FiberId", id: ++fiberIdCounter }) 84 149 85 - // Get the current fiber's ID 150 + /** 151 + * Get the current fiber's ID. 152 + */ 86 153 export const fiberId: Eff<FiberId, never, unknown> = 87 154 ({ _tag: "GetFiberId" }) 88 155 89 156 // === Transformations === 90 157 158 + /** 159 + * Chain effects together - the core composition operator. 160 + * 161 + * If the input effect succeeds, runs the function with the result. 162 + * If the input effect fails, the error passes through unchanged. 163 + * 164 + * This is like Promise.then(), but: 165 + * - Lazy (nothing runs until runPromise) 166 + * - Typed errors (E is tracked) 167 + * - Composable with other combinators 168 + * 169 + * @example 170 + * ```typescript 171 + * pipe( 172 + * succeed(10), 173 + * flatMap(n => succeed(n * 2)), 174 + * flatMap(n => succeed(n.toString())) 175 + * ) 176 + * // Eff<string, never, unknown> 177 + * ``` 178 + */ 91 179 export const flatMap = <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 92 180 (effect: Eff<A, E, R>): Eff<B, E, R> => 93 181 ({ _tag: "FlatMap", effect: effect as Eff<unknown, E, R>, f: f as (a: unknown) => Eff<B, E, R> }) 94 182 183 + /** 184 + * Transform the success value of an effect. 185 + * If the effect fails, the error passes through unchanged. 186 + * 187 + * @example 188 + * ```typescript 189 + * pipe( 190 + * succeed(10), 191 + * mapEff(n => n * 2) 192 + * ) 193 + * // Eff<number, never, unknown> with value 20 194 + * ``` 195 + */ 95 196 export const mapEff = <A, B, E, R>(f: (a: A) => B) => 96 197 (effect: Eff<A, E, R>): Eff<B, E, R> => 97 198 flatMap((a: A) => succeed(f(a)) as Eff<B, E, R>)(effect) 98 199 200 + /** 201 + * Handle both success and failure cases of an effect. 202 + * 203 + * @example 204 + * ```typescript 205 + * pipe( 206 + * someEffect, 207 + * foldEff( 208 + * error => succeed(`Error: ${error}`), 209 + * value => succeed(`Success: ${value}`) 210 + * ) 211 + * ) 212 + * ``` 213 + */ 99 214 export const foldEff = <A, B, E, E2, R>( 100 215 onErr: (e: E) => Eff<B, E2, R>, 101 216 onSucc: (a: A) => Eff<B, E2, R> ··· 107 222 onSucc: onSucc as (a: unknown) => Eff<B, E2, R> 108 223 }) 109 224 225 + /** 226 + * Recover from errors by providing a fallback effect. 227 + * 228 + * @example 229 + * ```typescript 230 + * pipe( 231 + * fail({ _tag: "NetworkError" }), 232 + * catchAll(error => succeed("fallback value")) 233 + * ) 234 + * ``` 235 + */ 110 236 export const catchAll = <A, E, E2, R>(onErr: (e: E) => Eff<A, E2, R>) => 111 237 (effect: Eff<A, E, R>): Eff<A, E2, R> => 112 238 foldEff(onErr, (a: A) => succeed(a))(effect) 113 239 114 240 // === Environment === 115 241 242 + /** 243 + * Access a value from the environment. 244 + * 245 + * @example 246 + * ```typescript 247 + * type Config = { apiUrl: string } 248 + * 249 + * const getUrl = access((config: Config) => config.apiUrl) 250 + * // Eff<string, never, Config> 251 + * ``` 252 + */ 116 253 export const access = <R, A>(f: (r: R) => A): Eff<A, never, R> => 117 254 ({ _tag: "Access", f }) 118 255 256 + /** 257 + * Access the environment and run an effect with it. 258 + * 259 + * @example 260 + * ```typescript 261 + * type Logger = { log: (msg: string) => void } 262 + * 263 + * const logMessage = (msg: string) => 264 + * accessEff((logger: Logger) => { 265 + * logger.log(msg) 266 + * return succeed(undefined) 267 + * }) 268 + * ``` 269 + */ 119 270 export const accessEff = <R, A, E>(f: (r: R) => Eff<A, E, R>): Eff<A, E, R> => 120 271 ({ _tag: "AccessEff", f }) 121 272 273 + /** 274 + * Provide an environment to an effect, removing the requirement. 275 + * 276 + * This is how you inject dependencies. Create an effect that requires 277 + * an environment, then use provide() to supply it. 278 + * 279 + * @example 280 + * ```typescript 281 + * type DbEnv = { db: Database } 282 + * 283 + * const queryUsers: Eff<User[], DbError, DbEnv> = ... 284 + * 285 + * // Provide the database, removing the requirement 286 + * const runnable = pipe( 287 + * queryUsers, 288 + * provide({ db: productionDb }) 289 + * ) 290 + * // Eff<User[], DbError, unknown> 291 + * ``` 292 + */ 122 293 export const provide = <R>(env: R) => 123 294 <A, E>(effect: Eff<A, E, R>): Eff<A, E, unknown> => 124 295 ({ _tag: "Provide", effect: effect as Eff<A, E, unknown>, env }) 125 296 126 297 // === Concurrency === 127 298 299 + /** 300 + * Fork an effect to run in the background. 301 + * Returns a Fiber that can be joined or interrupted. 302 + * 303 + * @example 304 + * ```typescript 305 + * const fiber = await runPromise(fork(longRunningEffect)) 306 + * // ... do other work ... 307 + * const result = await fiber.await() 308 + * ``` 309 + */ 128 310 export const fork = <A, E, R>(effect: Eff<A, E, R>): Eff<Fiber<A, E>, never, R> => 129 311 ({ _tag: "Fork", effect }) as unknown as Eff<Fiber<A, E>, never, R> 130 312 131 - // Fiber interface - implemented in trampoline 313 + /** 314 + * Fiber - a lightweight thread of execution. 315 + * 316 + * Fibers are the concurrency primitive in purus. They can be: 317 + * - Awaited (get the result as a Promise) 318 + * - Joined (get the result as an Effect) 319 + * - Interrupted (cancel the fiber) 320 + */ 132 321 export interface Fiber<A, E> { 133 322 readonly _tag: "Fiber" 134 323 readonly id: string ··· 139 328 140 329 // === Convenience Constructors === 141 330 331 + /** 332 + * Wrap a synchronous function that might throw. 333 + * Caught exceptions become the error channel. 334 + * 335 + * @example 336 + * ```typescript 337 + * const parsed = attempt(() => JSON.parse(input)) 338 + * // Eff<unknown, unknown, unknown> 339 + * ``` 340 + */ 142 341 export const attempt = <A>(f: () => A): Eff<A, unknown, unknown> => 143 342 async(resume => { 144 343 try { ··· 149 348 return () => {} 150 349 }) 151 350 351 + /** 352 + * Wrap a Promise-returning function. 353 + * The function is wrapped in a thunk to delay execution. 354 + * 355 + * @example 356 + * ```typescript 357 + * const fetchData = fromPromise(() => fetch("/api/data").then(r => r.json())) 358 + * // Eff<unknown, unknown, unknown> 359 + * ``` 360 + */ 152 361 export const fromPromise = <A>(f: () => Promise<A>): Eff<A, unknown, unknown> => 153 362 async(resume => { 154 363 f() ··· 157 366 return () => {} 158 367 }) 159 368 369 + /** 370 + * Sleep for a specified number of milliseconds. 371 + * Can be interrupted/cancelled. 372 + * 373 + * @example 374 + * ```typescript 375 + * pipe( 376 + * sleep(1000), 377 + * flatMap(() => succeed("Done waiting!")) 378 + * ) 379 + * ``` 380 + */ 160 381 export const sleep = (ms: number): Eff<void, never, unknown> => 161 382 async(resume => { 162 383 const id = setTimeout(() => resume({ _tag: "Success", value: undefined }), ms)
+136 -9
src/prelude/compose.ts
··· 1 - // Pure composition utilities for purus-ts prelude layer 1 + /** 2 + * Composition utilities for purus-ts. 3 + * 4 + * The pipe() and flow() functions are the primary way to compose 5 + * operations in purus. They enable readable, left-to-right data flow. 6 + * 7 + * @module prelude/compose 8 + */ 2 9 3 - // Identity function 10 + /** 11 + * Identity function - returns its argument unchanged. 12 + * 13 + * @example 14 + * ```typescript 15 + * id(42) // 42 16 + * id("hello") // "hello" 17 + * ``` 18 + */ 4 19 export const id = <A>(a: A): A => a 5 20 6 - // Constant function 21 + /** 22 + * Constant function - ignores its argument, always returns the captured value. 23 + * 24 + * @example 25 + * ```typescript 26 + * const always42 = constant(42) 27 + * always42() // 42 28 + * always42() // 42 29 + * ``` 30 + */ 7 31 export const constant = <A>(a: A) => (): A => a 8 32 9 - // Flip curried function arguments 33 + /** 34 + * Flip the arguments of a curried binary function. 35 + * 36 + * @example 37 + * ```typescript 38 + * const subtract = (a: number) => (b: number) => a - b 39 + * const flipped = flip(subtract) 40 + * subtract(10)(3) // 7 41 + * flipped(10)(3) // -7 42 + * ``` 43 + */ 10 44 export const flip = <A, B, C>(f: (a: A) => (b: B) => C) => 11 45 (b: B) => 12 46 (a: A): C => 13 47 f(a)(b) 14 48 15 - // Side effect tap 49 + /** 50 + * Execute a side effect and return the original value. 51 + * Useful for debugging or logging in pipelines. 52 + * 53 + * @example 54 + * ```typescript 55 + * pipe( 56 + * 42, 57 + * tap(x => console.log(`Value: ${x}`)), 58 + * x => x * 2 59 + * ) 60 + * // Logs: "Value: 42" 61 + * // Returns: 84 62 + * ``` 63 + */ 16 64 export const tap = <A>(f: (a: A) => void) => 17 65 (a: A): A => { 18 66 f(a) 19 67 return a 20 68 } 21 69 22 - // Trace - logging tap 70 + /** 71 + * Log a value with a label. Convenience wrapper around tap(). 72 + * 73 + * @example 74 + * ```typescript 75 + * pipe( 76 + * 42, 77 + * trace("before"), // Logs: "before 42" 78 + * x => x * 2, 79 + * trace("after") // Logs: "after 84" 80 + * ) 81 + * ``` 82 + */ 23 83 export const trace = <T>(label: string) => tap<T>(x => console.log(label, x)) 24 84 25 - // Conditional helper 85 + /** 86 + * Conditional helper - choose between two functions based on a predicate. 87 + * 88 + * @example 89 + * ```typescript 90 + * const result = ifElse(true)( 91 + * () => "was false", 92 + * () => "was true" 93 + * ) 94 + * // Returns: "was true" 95 + * ``` 96 + */ 26 97 export const ifElse = <T, E>(predicate: boolean) => 27 98 (onFalse: () => T | E, onTrue: () => T | E): T | E => 28 99 predicate ? onFalse() : onTrue() 29 100 30 - // pipe overloads (at least 6 overloads for type inference) 101 + /** 102 + * Pipe a value through a series of transformations. 103 + * 104 + * This is the primary composition tool in purus. It enables readable, 105 + * left-to-right data flow instead of deeply nested function calls. 106 + * 107 + * @example 108 + * ```typescript 109 + * // Without pipe (inside-out, hard to read): 110 + * mapEff(String)(flatMap(n => succeed(n + 5))(mapEff(n => n * 2)(succeed(10)))) 111 + * 112 + * // With pipe (top-to-bottom, easy to read): 113 + * pipe( 114 + * succeed(10), 115 + * mapEff(n => n * 2), 116 + * flatMap(n => succeed(n + 5)), 117 + * mapEff(String) 118 + * ) 119 + * ``` 120 + * 121 + * @example 122 + * ```typescript 123 + * // Works with any types, not just Effects: 124 + * const result = pipe( 125 + * " hello world ", 126 + * s => s.trim(), 127 + * s => s.toUpperCase(), 128 + * s => s.split(" ") 129 + * ) 130 + * // ["HELLO", "WORLD"] 131 + * ``` 132 + */ 31 133 export function pipe<A>(a: A): A 32 134 export function pipe<A, B>(a: A, ab: (a: A) => B): B 33 135 export function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C ··· 59 161 return fns.reduce((acc, fn) => fn(acc), a) 60 162 } 61 163 62 - // flow overloads (at least 4) 164 + /** 165 + * Compose functions left-to-right into a new function. 166 + * 167 + * Unlike pipe() which takes a value, flow() creates a reusable function. 168 + * Use flow() when you want to define a transformation to use later. 169 + * 170 + * @example 171 + * ```typescript 172 + * // Create a reusable transformation 173 + * const processString = flow( 174 + * (s: string) => s.trim(), 175 + * s => s.toUpperCase(), 176 + * s => s.split(" ") 177 + * ) 178 + * 179 + * processString(" hello world ") // ["HELLO", "WORLD"] 180 + * processString(" foo bar ") // ["FOO", "BAR"] 181 + * ``` 182 + * 183 + * @example 184 + * ```typescript 185 + * // Compare with pipe: 186 + * // pipe() applies immediately: pipe(value, f, g, h) 187 + * // flow() creates a function: flow(f, g, h)(value) 188 + * ``` 189 + */ 63 190 export function flow<A, B>(ab: (a: A) => B): (a: A) => B 64 191 export function flow<A, B, C>(ab: (a: A) => B, bc: (b: B) => C): (a: A) => C 65 192 export function flow<A, B, C, D>(
+114 -4
src/prelude/match.ts
··· 1 + /** 2 + * Pattern matching utilities for discriminated unions. 3 + * 4 + * These functions enable exhaustive, type-safe matching on tagged unions. 5 + * If you forget to handle a case, TypeScript complains at compile time. 6 + * 7 + * @module prelude/match 8 + */ 9 + 1 10 /** 2 11 * Exhaustive pattern matching for discriminated unions. 3 - * Requires handlers for all variants - compiler enforces exhaustiveness. 12 + * 13 + * Requires handlers for ALL variants of the union. If you forget one, 14 + * TypeScript gives a compile error. This prevents "forgot to handle 15 + * the new case" bugs. 16 + * 17 + * The union must have a `_tag` field as its discriminant. 18 + * 19 + * @example 20 + * ```typescript 21 + * type Shape = 22 + * | { _tag: "Circle"; radius: number } 23 + * | { _tag: "Rectangle"; width: number; height: number } 24 + * 25 + * const area = (shape: Shape): number => 26 + * match(shape)({ 27 + * Circle: ({ radius }) => Math.PI * radius ** 2, 28 + * Rectangle: ({ width, height }) => width * height, 29 + * }) 30 + * 31 + * // Try removing a case - TypeScript will error: 32 + * // "Property 'Rectangle' is missing in type..." 33 + * ``` 34 + * 35 + * @example 36 + * ```typescript 37 + * // Works great with Result and Option: 38 + * const message = match(result)({ 39 + * Ok: ({ value }) => `Success: ${value}`, 40 + * Err: ({ error }) => `Failed: ${error}`, 41 + * }) 42 + * ``` 4 43 */ 5 44 export const match = <T extends { _tag: string }>(value: T) => 6 45 <R>(cases: { [K in T["_tag"]]: (v: Extract<T, { _tag: K }>) => R }): R => 7 46 (cases as unknown as Record<string, (v: T) => R>)[value._tag]!(value) 8 47 9 48 /** 10 - * Pattern matching with default case for partial matches. 49 + * Pattern matching with a default case for partial matches. 50 + * 51 + * Unlike match(), you don't need to handle every variant. 52 + * Unhandled cases return the default value. 53 + * 54 + * @example 55 + * ```typescript 56 + * type HttpStatus = 57 + * | { _tag: "Ok"; data: unknown } 58 + * | { _tag: "NotFound" } 59 + * | { _tag: "ServerError"; code: number } 60 + * | { _tag: "Timeout" } 61 + * 62 + * // Only handle specific cases, default for the rest 63 + * const message = matchOr("Unknown error")(status)({ 64 + * Ok: () => "Success", 65 + * NotFound: () => "Not found", 66 + * }) 67 + * ``` 11 68 */ 12 69 export const matchOr = <T extends { _tag: string }, R>(defaultValue: R) => 13 70 (value: T) => ··· 18 75 19 76 /** 20 77 * Guard-based pattern matching with predicates. 21 - * Evaluates guards in order, returns first match or default. 78 + * 79 + * Unlike match() which dispatches on _tag, when() uses predicate functions. 80 + * Guards are evaluated in order - first matching guard wins. 81 + * 82 + * @example 83 + * ```typescript 84 + * const classify = (n: number): string => 85 + * when(n)( 86 + * [x => x < 0, () => "negative"], 87 + * [x => x === 0, () => "zero"], 88 + * [x => x < 10, () => "small positive"], 89 + * )(() => "large positive") 90 + * 91 + * classify(-5) // "negative" 92 + * classify(0) // "zero" 93 + * classify(7) // "small positive" 94 + * classify(100) // "large positive" 95 + * ``` 96 + * 97 + * @example 98 + * ```typescript 99 + * // Useful for complex conditions: 100 + * const grade = when(score)( 101 + * [s => s >= 90, () => "A"], 102 + * [s => s >= 80, () => "B"], 103 + * [s => s >= 70, () => "C"], 104 + * [s => s >= 60, () => "D"], 105 + * )(() => "F") 106 + * ``` 22 107 */ 23 108 export const when = <T>(value: T) => 24 109 <R>(...guards: ReadonlyArray<readonly [(v: T) => boolean, (v: T) => R]>) => ··· 33 118 } 34 119 35 120 /** 36 - * Literal value matching. 121 + * Match on literal values (strings, numbers, booleans). 122 + * 123 + * Unlike match() which works on objects with _tag, this matches 124 + * primitive literal values directly. 125 + * 126 + * @example 127 + * ```typescript 128 + * type Direction = "north" | "south" | "east" | "west" 129 + * 130 + * const dx = (dir: Direction): number => 131 + * matchLiteral(dir)({ 132 + * north: 0, 133 + * south: 0, 134 + * east: 1, 135 + * west: -1, 136 + * }) 137 + * ``` 138 + * 139 + * @example 140 + * ```typescript 141 + * // With a default case: 142 + * const dayType = matchLiteral(day)({ 143 + * saturday: "weekend", 144 + * sunday: "weekend", 145 + * }, "weekday") 146 + * ``` 37 147 */ 38 148 export const matchLiteral = <T extends string | number | boolean>(value: T) => 39 149 <R>(cases: { [K in T & PropertyKey]: R }, defaultCase?: R): R =>
+142 -5
src/prelude/option.ts
··· 1 - // Types 1 + /** 2 + * Option type for nullable values. 3 + * 4 + * Option<T> represents either a value (Some<T>) or nothing (None). 5 + * Use this instead of null/undefined for composable null handling. 6 + * 7 + * @example 8 + * ```typescript 9 + * const findUser = (id: string): Option<User> => 10 + * users.has(id) ? some(users.get(id)!) : none 11 + * 12 + * pipe( 13 + * findUser("123"), 14 + * mapOption(user => user.name), 15 + * getOrElse("Unknown") 16 + * ) 17 + * ``` 18 + * 19 + * @module prelude/option 20 + */ 21 + 22 + /** 23 + * Some variant of Option. Contains a value of type T. 24 + */ 2 25 export type Some<T> = { readonly _tag: "Some"; readonly value: T } 26 + 27 + /** 28 + * None variant of Option. Represents absence of a value. 29 + */ 3 30 export type None = { readonly _tag: "None" } 31 + 32 + /** 33 + * Option<T> - Either a value T or nothing. 34 + * 35 + * Use this for values that might not exist. Unlike null/undefined, 36 + * Option forces explicit handling of the empty case. 37 + * 38 + * @template T - The value type when present 39 + */ 4 40 export type Option<T> = Some<T> | None 5 41 6 - // Constructors 42 + /** 43 + * Create an Option containing a value. 44 + * 45 + * @example 46 + * ```typescript 47 + * const opt = some(42) 48 + * // Type: Some<number> 49 + * ``` 50 + */ 7 51 export const some = <T>(value: T): Some<T> => ({ _tag: "Some", value }) 52 + 53 + /** 54 + * The empty Option - represents absence of a value. 55 + * 56 + * @example 57 + * ```typescript 58 + * const empty: Option<number> = none 59 + * ``` 60 + */ 8 61 export const none: None = { _tag: "None" } 9 62 10 - // Type guards 63 + /** 64 + * Type guard to check if an Option contains a value. 65 + * 66 + * @example 67 + * ```typescript 68 + * if (isSome(opt)) { 69 + * console.log(opt.value) // TypeScript knows it's Some 70 + * } 71 + * ``` 72 + */ 11 73 export const isSome = <T>(o: Option<T>): o is Some<T> => o._tag === "Some" 74 + 75 + /** 76 + * Type guard to check if an Option is empty. 77 + * 78 + * @example 79 + * ```typescript 80 + * if (isNone(opt)) { 81 + * console.log("No value") 82 + * } 83 + * ``` 84 + */ 12 85 export const isNone = <T>(o: Option<T>): o is None => o._tag === "None" 13 86 14 - // From nullable 87 + /** 88 + * Convert a nullable value to an Option. 89 + * null and undefined become None, everything else becomes Some. 90 + * 91 + * @example 92 + * ```typescript 93 + * fromNullable(42) // Some(42) 94 + * fromNullable(null) // None 95 + * fromNullable(undefined) // None 96 + * ``` 97 + */ 15 98 export const fromNullable = <T>(value: T | null | undefined): Option<T> => 16 99 value == null ? none : some(value) 17 100 18 - // Operations - expression only 101 + /** 102 + * Transform the value inside an Option. 103 + * If None, returns None unchanged. 104 + * 105 + * @example 106 + * ```typescript 107 + * pipe(some(10), mapOption(n => n * 2)) // Some(20) 108 + * pipe(none, mapOption(n => n * 2)) // None 109 + * ``` 110 + */ 19 111 export const mapOption = <T, U>(f: (t: T) => U) => 20 112 (option: Option<T>): Option<U> => 21 113 option._tag === "Some" ? some(f(option.value)) : none 22 114 115 + /** 116 + * Chain Option-returning operations. 117 + * If Some, runs the function. If None, returns None. 118 + * 119 + * @example 120 + * ```typescript 121 + * const findUser = (id: string): Option<User> => ... 122 + * const getEmail = (user: User): Option<string> => ... 123 + * 124 + * pipe( 125 + * findUser("123"), 126 + * flatMapOption(getEmail) 127 + * ) // Some(email) or None 128 + * ``` 129 + */ 23 130 export const flatMapOption = <T, U>(f: (t: T) => Option<U>) => 24 131 (option: Option<T>): Option<U> => 25 132 option._tag === "Some" ? f(option.value) : none 26 133 134 + /** 135 + * Extract the value from an Option, or return a default if None. 136 + * 137 + * @example 138 + * ```typescript 139 + * pipe(some(42), getOrElse(0)) // 42 140 + * pipe(none, getOrElse(0)) // 0 141 + * ``` 142 + */ 27 143 export const getOrElse = <T>(defaultValue: T) => 28 144 (option: Option<T>): T => 29 145 option._tag === "Some" ? option.value : defaultValue 30 146 147 + /** 148 + * Pattern match on an Option, handling both Some and None cases. 149 + * 150 + * @example 151 + * ```typescript 152 + * matchOption( 153 + * value => `Found: ${value}`, 154 + * () => "Not found" 155 + * )(option) 156 + * ``` 157 + */ 31 158 export const matchOption = <T, R>( 32 159 onSome: (value: T) => R, 33 160 onNone: () => R 34 161 ) => (option: Option<T>): R => 35 162 option._tag === "Some" ? onSome(option.value) : onNone() 36 163 164 + /** 165 + * Convert an Option to a nullable value. 166 + * Some becomes the value, None becomes null. 167 + * 168 + * @example 169 + * ```typescript 170 + * toNullable(some(42)) // 42 171 + * toNullable(none) // null 172 + * ``` 173 + */ 37 174 export const toNullable = <T>(option: Option<T>): T | null => 38 175 option._tag === "Some" ? option.value : null
+148 -5
src/prelude/result.ts
··· 1 - // Result type for error handling as values 1 + /** 2 + * Result type for error handling as values. 3 + * 4 + * Result<T, E> represents either success (Ok<T>) or failure (Err<E>). 5 + * Unlike exceptions, the error type is tracked at compile time. 6 + * 7 + * @example 8 + * ```typescript 9 + * const divide = (a: number, b: number): Result<number, string> => 10 + * b === 0 ? err("Division by zero") : ok(a / b) 11 + * 12 + * const result = divide(10, 2) 13 + * matchResult( 14 + * value => console.log(`Result: ${value}`), 15 + * error => console.log(`Error: ${error}`) 16 + * )(result) 17 + * ``` 18 + * 19 + * @module prelude/result 20 + */ 2 21 3 - // Types 22 + /** 23 + * Success variant of Result. Contains a value of type T. 24 + */ 4 25 export type Ok<T> = { readonly _tag: "Ok"; readonly value: T } 26 + 27 + /** 28 + * Failure variant of Result. Contains an error of type E. 29 + */ 5 30 export type Err<E> = { readonly _tag: "Err"; readonly error: E } 31 + 32 + /** 33 + * Result<T, E> - Either a success value T or an error E. 34 + * 35 + * Use this instead of throwing exceptions to make errors visible in types. 36 + * The _tag field enables exhaustive pattern matching with match(). 37 + * 38 + * @template T - The success value type 39 + * @template E - The error type 40 + */ 6 41 export type Result<T, E> = Ok<T> | Err<E> 7 42 8 - // Constructors 43 + /** 44 + * Create a success Result. 45 + * 46 + * @example 47 + * ```typescript 48 + * const result = ok(42) 49 + * // Type: Ok<number> 50 + * ``` 51 + */ 9 52 export const ok = <T>(value: T): Ok<T> => ({ _tag: "Ok", value }) 53 + 54 + /** 55 + * Create a failure Result. 56 + * 57 + * @example 58 + * ```typescript 59 + * const result = err({ _tag: "NotFound", id: "123" }) 60 + * // Type: Err<{ _tag: "NotFound"; id: string }> 61 + * ``` 62 + */ 10 63 export const err = <E>(error: E): Err<E> => ({ _tag: "Err", error }) 11 64 12 - // Type guards 65 + /** 66 + * Type guard to check if a Result is Ok. 67 + * 68 + * @example 69 + * ```typescript 70 + * if (isOk(result)) { 71 + * console.log(result.value) // TypeScript knows it's Ok 72 + * } 73 + * ``` 74 + */ 13 75 export const isOk = <T, E>(r: Result<T, E>): r is Ok<T> => r._tag === "Ok" 76 + 77 + /** 78 + * Type guard to check if a Result is Err. 79 + * 80 + * @example 81 + * ```typescript 82 + * if (isErr(result)) { 83 + * console.log(result.error) // TypeScript knows it's Err 84 + * } 85 + * ``` 86 + */ 14 87 export const isErr = <T, E>(r: Result<T, E>): r is Err<E> => r._tag === "Err" 15 88 16 - // Operations - expression only, no early returns 89 + /** 90 + * Transform the success value of a Result. 91 + * If the Result is Err, it passes through unchanged. 92 + * 93 + * @example 94 + * ```typescript 95 + * pipe( 96 + * ok(10), 97 + * mapResult(n => n * 2) 98 + * ) // Ok(20) 99 + * 100 + * pipe( 101 + * err("oops"), 102 + * mapResult(n => n * 2) 103 + * ) // Err("oops") - unchanged 104 + * ``` 105 + */ 17 106 export const mapResult = <T, U, E>(f: (t: T) => U) => 18 107 (result: Result<T, E>): Result<U, E> => 19 108 result._tag === "Ok" ? ok(f(result.value)) : result 20 109 110 + /** 111 + * Transform the error value of a Result. 112 + * If the Result is Ok, it passes through unchanged. 113 + * 114 + * @example 115 + * ```typescript 116 + * pipe( 117 + * err("raw error"), 118 + * mapErr(msg => ({ _tag: "FormattedError", message: msg })) 119 + * ) 120 + * ``` 121 + */ 21 122 export const mapErr = <T, E, F>(f: (e: E) => F) => 22 123 (result: Result<T, E>): Result<T, F> => 23 124 result._tag === "Err" ? err(f(result.error)) : result 24 125 126 + /** 127 + * Chain Result-returning operations. 128 + * If the input is Ok, runs the function. If Err, passes through unchanged. 129 + * 130 + * This is the core composition operator for Results - it lets you 131 + * sequence operations where each can fail. 132 + * 133 + * @example 134 + * ```typescript 135 + * const divide = (a: number, b: number): Result<number, string> => 136 + * b === 0 ? err("Division by zero") : ok(a / b) 137 + * 138 + * pipe( 139 + * ok(100), 140 + * chainResult(n => divide(n, 5)), // Ok(20) 141 + * chainResult(n => divide(n, 0)), // Err("Division by zero") 142 + * chainResult(n => divide(n, 2)) // Never runs 143 + * ) 144 + * ``` 145 + */ 25 146 export const chainResult = <T, U, E>(f: (t: T) => Result<U, E>) => 26 147 (result: Result<T, E>): Result<U, E> => 27 148 result._tag === "Ok" ? f(result.value) : result 28 149 150 + /** 151 + * Extract the success value, or return a default if Err. 152 + * 153 + * @example 154 + * ```typescript 155 + * pipe(ok(42), unwrapOr(0)) // 42 156 + * pipe(err("oops"), unwrapOr(0)) // 0 157 + * ``` 158 + */ 29 159 export const unwrapOr = <T, E>(defaultValue: T) => 30 160 (result: Result<T, E>): T => 31 161 result._tag === "Ok" ? result.value : defaultValue 32 162 163 + /** 164 + * Pattern match on a Result, handling both Ok and Err cases. 165 + * 166 + * @example 167 + * ```typescript 168 + * matchResult( 169 + * value => `Got: ${value}`, 170 + * error => `Failed: ${error}` 171 + * )(result) 172 + * ``` 173 + */ 33 174 export const matchResult = <T, E, R>( 34 175 onOk: (value: T) => R, 35 176 onErr: (error: E) => R ··· 47 188 * Inside purus, all error handling uses Result - no exceptions thrown. 48 189 * 49 190 * @example 191 + * ```typescript 50 192 * // Safely parse untrusted JSON 51 193 * const result = tryCatch(() => JSON.parse(userInput)) 52 194 * // Result<unknown, unknown> - exception becomes Err, no throws! ··· 57 199 * mapResult(transform), 58 200 * unwrapOr(defaultValue) 59 201 * ) 202 + * ``` 60 203 */ 61 204 export const tryCatch = <T>(f: () => T): Result<T, unknown> => { 62 205 try {