An educational pure functional programming library in TypeScript
2
fork

Configure Feed

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

Improve examples and docs with pattern matching, O(n) arrays, and clarifications

+51 -13
+1
docs/guides/README.md
··· 47 47 48 48 | Article | Description | 49 49 |---------------------------------------------------------|--------------------------------------------------------| 50 + | [Why These Strange Names?](./concepts/00-why-these-strange-names.md) | Demystifying FP terminology | 50 51 | [Why Errors as Values?](./concepts/01-errors-as-values.md) | The exception problem, railway oriented programming | 51 52 | [Branded Types In Depth](./concepts/02-branded-types.md) | Phantom types, smart constructors, production patterns | 52 53 | [Typestate Pattern](./concepts/05-typestate-pattern.md) | State machines in the type system |
+2
docs/guides/concepts/02-branded-types.md
··· 212 212 // because it bridges impure JavaScript APIs 213 213 const Url = (s: string): Option<Url> => { 214 214 try { 215 + // Comma operator: evaluates new URL(s) which throws if invalid, 216 + // then returns the second expression (the branded value) 215 217 return (new URL(s), some(s as Url)) 216 218 } catch { 217 219 return none
+23 -1
docs/guides/concepts/04-type-classes-in-typescript.md
··· 98 98 99 99 ### Workarounds Exist But Are Complex 100 100 101 - Libraries like fp-ts use encoding tricks (TypeScript's module augmentation) to simulate HKT. purus opts for simplicity: explicit functions per type, consistent patterns. 101 + Libraries like fp-ts use encoding tricks to simulate HKT. Here's a simplified view: 102 + 103 + ```typescript 104 + // fp-ts style: types register themselves via module augmentation 105 + interface URItoKind<A> { 106 + Result: Result<A, unknown> 107 + Option: Option<A> 108 + } 109 + 110 + type Kind<URI, A> = URItoKind<A>[URI] 111 + 112 + // Now you can write generic code 113 + const map = <F extends keyof URItoKind<unknown>, A, B>( 114 + F: Functor<F>, 115 + f: (a: A) => B 116 + ) => (fa: Kind<F, A>): Kind<F, B> => F.map(f)(fa) 117 + ``` 118 + 119 + This works but adds complexity: URI strings, module augmentation, and type-level lookups. 120 + 121 + **purus opts for simplicity:** explicit functions per type (`mapResult`, `mapOption`, `mapEff`) with consistent patterns. You lose some abstraction but gain immediate readability. 102 122 103 123 > **New to FP terminology?** Read [Why These Strange Names?](./00-why-these-strange-names.md) first for plain-English explanations and real-world analogies. 104 124 ··· 331 351 ``` 332 352 333 353 The function `f` returns a new wrapped value, and `flatMap` "flattens" the result (avoiding `F<F<B>>`). 354 + 355 + > **purus naming:** For Result, the flatMap operation is named `chainResult` to match the type. For Eff, it's `flatMap`. Both implement the same Monad pattern. 334 356 335 357 ### Why Monad Matters 336 358
+12
docs/guides/tutorial/02-your-first-effect.md
··· 147 147 148 148 The function returns a Promise. It's wrapped in a thunk `() =>` so it doesn't start immediately. 149 149 150 + **Why the thunk matters:** 151 + 152 + ```typescript 153 + // WITHOUT thunk — fetch starts immediately 154 + const eager = fromPromise(fetch("/api")) // Request already in flight! 155 + 156 + // WITH thunk — nothing happens until runPromise 157 + const lazy = fromPromise(() => fetch("/api")) // Deferred 158 + ``` 159 + 160 + The thunk defers execution, which is essential for composability. You can pass `lazy` around, combine it with other effects, add timeout or retry logic—all before any network request happens. 161 + 150 162 ### async() - Full control over async behavior 151 163 152 164 For advanced cases where you need cleanup or cancellation:
+5 -3
examples/task-queue/with-purus.ts
··· 41 41 retry, 42 42 allSequential, 43 43 pipe, 44 + match, 44 45 runPromise, 45 46 } from "../../src/index" 46 47 ··· 218 219 catchAll((error: JobError) => 219 220 accessEff((env: QueueEnv) => ( 220 221 env.logger.error( 221 - error._tag === "TimeoutError" 222 - ? `Job ${error.jobId} timed out after ${error.ms}ms` 223 - : `Job ${error.jobId} failed: ${error.message}` 222 + match(error)({ 223 + TimeoutError: ({ jobId, ms }) => `Job ${jobId} timed out after ${ms}ms`, 224 + TransientError: ({ jobId, message }) => `Job ${jobId} failed: ${message}`, 225 + }) 224 226 ), 225 227 succeed(undefined) 226 228 ))
+8 -9
examples/user-registration/with-purus.ts
··· 229 229 return pipe( 230 230 users, 231 231 traversePar(createAndCapture), 232 - mapEff((results) => 233 - results.reduce<BatchResult>( 234 - (acc, result) => 235 - result.ok 236 - ? { ...acc, successful: [...acc.successful, result.user] } 237 - : { ...acc, failed: [...acc.failed, { input: result.input, error: result.error }] }, 238 - { successful: [], failed: [] }, 239 - ), 240 - ), 232 + mapEff((results) => ({ 233 + successful: results 234 + .filter((r): r is { ok: true; user: User } => r.ok) 235 + .map((r) => r.user), 236 + failed: results 237 + .filter((r): r is { ok: false; input: UserInput; error: ApiError } => !r.ok) 238 + .map((r) => ({ input: r.input, error: r.error })), 239 + })), 241 240 ) 242 241 } 243 242