An educational pure functional programming library in TypeScript
2
fork

Configure Feed

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

Tighten traverser FP purity, consolidate Exit namespace, clean redundant typeclass comments, and bump dev dependencies

+61 -71
+15 -15
bun.lock
··· 5 5 "": { 6 6 "name": "purus-ts", 7 7 "devDependencies": { 8 - "@biomejs/biome": "^2.3.12", 9 - "@types/bun": "^1.1.0", 10 - "typescript": "^5", 8 + "@biomejs/biome": "^2.4.12", 9 + "@types/bun": "^1.3.12", 10 + "typescript": "^6.0.3", 11 11 }, 12 12 }, 13 13 }, 14 14 "packages": { 15 - "@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], 15 + "@biomejs/biome": ["@biomejs/biome@2.4.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.12", "@biomejs/cli-darwin-x64": "2.4.12", "@biomejs/cli-linux-arm64": "2.4.12", "@biomejs/cli-linux-arm64-musl": "2.4.12", "@biomejs/cli-linux-x64": "2.4.12", "@biomejs/cli-linux-x64-musl": "2.4.12", "@biomejs/cli-win32-arm64": "2.4.12", "@biomejs/cli-win32-x64": "2.4.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Rro7adQl3NLq/zJCIL98eElXKI8eEiBtoeu5TbXF/U3qbjuSc7Jb5rjUbeHHcquDWeSf3HnGP7XI5qGrlRk/pA=="], 16 16 17 - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], 17 + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BnMU4Pc3ciEVteVpZ0BK33MLr7X57F5w1dwDLDn+/iy/yTrA4Q/N2yftidFtsA4vrDh0FMXDpacNV/Tl3fbmng=="], 18 18 19 - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], 19 + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-x9uJ0bI1rJsWICp3VH8w/5PnAVD3A7SqzDpbrfoUQX1QyWrK5jSU4fRLo/wSgGeplCivbxBRKmt5Xq4/nWvq8A=="], 20 20 21 - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="], 21 + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw=="], 22 22 23 - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="], 23 + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FhfpkAAlKL6kwvcVap0Hgp4AhZmtd3YImg0kK1jd7C/aSoh4SfsB2f++yG1rU0lr8Y5MCFJrcSkmssiL9Xnnig=="], 24 24 25 - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="], 25 + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw=="], 26 26 27 - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="], 27 + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.12", "", { "os": "linux", "cpu": "x64" }, "sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew=="], 28 28 29 - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="], 29 + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig=="], 30 30 31 - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], 31 + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.12", "", { "os": "win32", "cpu": "x64" }, "sha512-yMckRzTyZ83hkk8iDFWswqSdU8tvZxspJKnYNh7JZr/zhZNOlzH13k4ecboU6MurKExCe2HUkH75pGI/O2JwGA=="], 32 32 33 - "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 33 + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], 34 34 35 35 "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 36 36 37 - "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 37 + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], 38 38 39 - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 39 + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], 40 40 41 41 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 42 42 }
+6 -6
docs-site/src/content/docs/examples/http-client.md
··· 319 319 // HOW IT WORKS: 320 320 // 1. async() takes a "register" function 321 321 // 2. The register function receives a "resume" callback 322 - // 3. You start your async work and call resume(Exit.succeed(value)) or resume(Exit.fail(error)) 322 + // 3. You start your async work and call resume(Exit.success(value)) or resume(Exit.failure(error)) 323 323 // 4. Return a cleanup function - it runs on cancellation 324 324 // 325 325 // GOTCHA: The AbortController abort() must happen in the cleanup function. ··· 337 337 .then((response) => 338 338 !response.ok 339 339 ? response.status === 404 340 - ? resume(Exit.fail(HttpError.notFound(url))) 341 - : resume(Exit.fail(HttpError.serverError(response.status))) 340 + ? resume(Exit.failure(HttpError.notFound(url))) 341 + : resume(Exit.failure(HttpError.serverError(response.status))) 342 342 : response.json().then((data: unknown) => 343 343 // Use type guard instead of casting - validates at runtime 344 344 isUser(data) 345 - ? resume(Exit.succeed(data)) 346 - : resume(Exit.fail(HttpError.serverError(500))), 345 + ? resume(Exit.success(data)) 346 + : resume(Exit.failure(HttpError.serverError(500))), 347 347 ), 348 348 ) 349 349 .catch((err) => { 350 350 // GOTCHA: Don't resume on AbortError - the fiber is already cancelled 351 351 // Resuming after cancellation would cause undefined behavior 352 352 if (err.name === "AbortError") return 353 - resume(Exit.fail(HttpError.network(err.message))) 353 + resume(Exit.failure(HttpError.network(err.message))) 354 354 }) 355 355 356 356 // THE KEY INSIGHT: This cleanup function runs automatically on timeout/interrupt
+2 -2
docs-site/src/content/docs/examples/task-queue.md
··· 400 400 401 401 // 30% chance of failure for demo purposes 402 402 if (Math.random() < 0.3) { 403 - resume(Exit.fail(JobError.transient(job.id, "transient error"))) 403 + resume(Exit.failure(JobError.transient(job.id, "transient error"))) 404 404 } else { 405 - resume(Exit.succeed(undefined)) 405 + resume(Exit.success(undefined)) 406 406 } 407 407 }, delay) 408 408
+1 -1
docs-site/src/content/docs/tutorial/02-your-first-effect.md
··· 172 172 import { async, Exit } from "purus-ts" 173 173 174 174 const delay = (ms: number) => async<void, never>(resume => { 175 - const id = setTimeout(() => resume(Exit.succeed(undefined)), ms) 175 + const id = setTimeout(() => resume(Exit.success(undefined)), ms) 176 176 return () => clearTimeout(id) // Cleanup function 177 177 }) 178 178 ```
+3 -3
docs-site/src/content/docs/tutorial/08-concurrency-with-fibers.md
··· 225 225 const cancellableDelay = (ms: number) => 226 226 async<void, never>(resume => { 227 227 const id = setTimeout( 228 - () => resume(Exit.succeed(undefined)), 228 + () => resume(Exit.success(undefined)), 229 229 ms 230 230 ) 231 231 ··· 247 247 const controller = new AbortController() 248 248 249 249 fetch(url, { signal: controller.signal }) 250 - .then(response => resume(Exit.succeed(response))) 250 + .then(response => resume(Exit.success(response))) 251 251 .catch(error => { 252 252 if (error.name !== "AbortError") { 253 - resume(Exit.fail(error)) 253 + resume(Exit.failure(error)) 254 254 } 255 255 // Don't resume on AbortError - fiber is already done 256 256 })
+1 -1
docs-site/src/content/docs/tutorial/10-building-a-complete-app.md
··· 322 322 323 323 const prompt = (rl: readline.Interface): Eff<string, never, unknown> => 324 324 async(resume => { 325 - rl.question("> ", answer => resume(Exit.succeed(answer))) 325 + rl.question("> ", answer => resume(Exit.success(answer))) 326 326 return () => {} 327 327 }) 328 328
+6 -6
examples/http-client/with-purus.ts
··· 90 90 // HOW IT WORKS: 91 91 // 1. async() takes a "register" function 92 92 // 2. The register function receives a "resume" callback 93 - // 3. You start your async work and call resume(Exit.succeed(value)) or resume(Exit.fail(error)) 93 + // 3. You start your async work and call resume(Exit.success(value)) or resume(Exit.failure(error)) 94 94 // 4. Return a cleanup function - it runs on cancellation 95 95 // 96 96 // GOTCHA: The AbortController abort() must happen in the cleanup function. ··· 108 108 .then((response) => 109 109 !response.ok 110 110 ? response.status === 404 111 - ? resume(Exit.fail(HttpError.notFound(url))) 112 - : resume(Exit.fail(HttpError.serverError(response.status))) 111 + ? resume(Exit.failure(HttpError.notFound(url))) 112 + : resume(Exit.failure(HttpError.serverError(response.status))) 113 113 : response.json().then((data: unknown) => 114 114 // Use type guard instead of casting - validates at runtime 115 115 isUser(data) 116 - ? resume(Exit.succeed(data)) 117 - : resume(Exit.fail(HttpError.serverError(500))), 116 + ? resume(Exit.success(data)) 117 + : resume(Exit.failure(HttpError.serverError(500))), 118 118 ), 119 119 ) 120 120 .catch((err) => { 121 121 // GOTCHA: Don't resume on AbortError - the fiber is already cancelled 122 122 // Resuming after cancellation would cause undefined behavior 123 123 if (err.name === "AbortError") return 124 - resume(Exit.fail(HttpError.network(err.message))) 124 + resume(Exit.failure(HttpError.network(err.message))) 125 125 }) 126 126 127 127 // THE KEY INSIGHT: This cleanup function runs automatically on timeout/interrupt
+2 -2
examples/task-queue/with-purus.ts
··· 156 156 157 157 // 30% chance of failure for demo purposes 158 158 if (Math.random() < 0.3) { 159 - resume(Exit.fail(JobError.transient(job.id, "transient error"))) 159 + resume(Exit.failure(JobError.transient(job.id, "transient error"))) 160 160 } else { 161 - resume(Exit.succeed(undefined)) 161 + resume(Exit.success(undefined)) 162 162 } 163 163 }, delay) 164 164
+3 -3
package.json
··· 41 41 }, 42 42 "homepage": "https://tangled.sh/oleksify.me/purus-ts", 43 43 "devDependencies": { 44 - "@biomejs/biome": "^2.3.12", 45 - "@types/bun": "^1.1.0", 46 - "typescript": "^5" 44 + "@biomejs/biome": "^2.4.12", 45 + "@types/bun": "^1.3.12", 46 + "typescript": "^6.0.3" 47 47 } 48 48 }
+1 -4
src/effect/exit.ts
··· 77 77 ? failure(onFailure(exit.error)) 78 78 : interrupted(exit.by) 79 79 80 - // Namespace for convenient access (using original naming convention) 80 + // Namespace for convenient dotted access to the Exit API. 81 81 export const Exit = { 82 - succeed: success, 83 - fail: failure, 84 - interrupt: interrupted, 85 82 success, 86 83 failure, 87 84 interrupted,
+9 -9
src/prelude/option.ts
··· 188 188 */ 189 189 export const traverseOption = 190 190 <A, B>(f: (a: A) => Option<B>) => 191 - (as: readonly A[]): Option<readonly B[]> => { 192 - const results: B[] = [] 193 - for (const a of as) { 194 - const o = f(a) 195 - if (o._tag === "None") return none 196 - results.push(o.value) 197 - } 198 - return some(results) 199 - } 191 + (as: readonly A[]): Option<readonly B[]> => 192 + as.reduce<Option<readonly B[]>>( 193 + (acc, a) => 194 + acc._tag === "None" 195 + ? acc 196 + : ((o: Option<B>): Option<readonly B[]> => 197 + o._tag === "None" ? none : some([...acc.value, o.value]))(f(a)), 198 + some([]), 199 + ) 200 200 201 201 /** 202 202 * Collapse an array of Options into an Option of an array.
+9 -9
src/prelude/result.ts
··· 252 252 */ 253 253 export const traverseResult = 254 254 <A, B, E>(f: (a: A) => Result<B, E>) => 255 - (as: readonly A[]): Result<readonly B[], E> => { 256 - const results: B[] = [] 257 - for (const a of as) { 258 - const r = f(a) 259 - if (r._tag === "Err") return r 260 - results.push(r.value) 261 - } 262 - return ok(results) 263 - } 255 + (as: readonly A[]): Result<readonly B[], E> => 256 + as.reduce<Result<readonly B[], E>>( 257 + (acc, a) => 258 + acc._tag === "Err" 259 + ? acc 260 + : ((r: Result<B, E>): Result<readonly B[], E> => 261 + r._tag === "Err" ? r : ok([...acc.value, r.value]))(f(a)), 262 + ok([]), 263 + ) 264 264 265 265 /** 266 266 * Collapse an array of Results into a Result of an array.
+3 -10
src/prelude/typeclasses.ts
··· 27 27 * @module prelude/typeclasses 28 28 */ 29 29 30 - import { flatMapOption, mapOption, type Option, some } from "./option" 30 + import { flatMapOption, mapOption, none, type Option, some } from "./option" 31 31 import { bimap, chainResult, mapResult, ok, type Result } from "./result" 32 32 33 33 // ----------------------------------------------------------------------------- 34 - // Type Class Interfaces (Educational) 35 - // ----------------------------------------------------------------------------- 36 - 37 - // ----------------------------------------------------------------------------- 38 34 // Conceptual Type Class Interfaces 39 35 // ----------------------------------------------------------------------------- 40 - // TypeScript lacks higher-kinded types (HKT), so we cannot express these 41 - // interfaces generically like "interface Functor<F[_]>". Instead, we document 42 - // the pattern conceptually. The instance objects below show concrete examples. 43 36 44 37 /** 45 38 * Functor - types that can be mapped over. ··· 179 172 <B>(of_: Option<(a: A) => B>): Option<B> => 180 173 of_._tag === "Some" && oa._tag === "Some" 181 174 ? some(of_.value(oa.value)) 182 - : { _tag: "None" } 175 + : none 183 176 184 177 // ----------------------------------------------------------------------------- 185 178 // Instance Objects (Demonstrating the Pattern) ··· 264 257 (oa: Option<A>, ob: Option<B>): Option<C> => 265 258 oa._tag === "Some" && ob._tag === "Some" 266 259 ? some(f(oa.value, ob.value)) 267 - : { _tag: "None" } 260 + : none