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 Biome linter with strict FP rules and fix test type errors

+1170 -582
+52
biome.json
··· 1 + { 2 + "$schema": "https://biomejs.dev/schemas/2.3.12/schema.json", 3 + "vcs": { 4 + "enabled": true, 5 + "clientKind": "git", 6 + "useIgnoreFile": true 7 + }, 8 + "files": { 9 + "includes": ["src/**/*.ts", "tests/**/*.ts"] 10 + }, 11 + "formatter": { 12 + "enabled": true, 13 + "indentStyle": "space", 14 + "indentWidth": 2 15 + }, 16 + "linter": { 17 + "enabled": true, 18 + "rules": { 19 + "recommended": true, 20 + "style": { 21 + "useConst": "error", 22 + "noParameterAssign": "error", 23 + "noNonNullAssertion": "off" 24 + }, 25 + "suspicious": { 26 + "noExplicitAny": "error", 27 + "useIterableCallbackReturn": "off" 28 + }, 29 + "correctness": { 30 + "noUnusedVariables": "error", 31 + "noUnusedImports": "error" 32 + }, 33 + "complexity": { 34 + "noCommaOperator": "off" 35 + } 36 + } 37 + }, 38 + "javascript": { 39 + "formatter": { 40 + "semicolons": "asNeeded", 41 + "quoteStyle": "double" 42 + } 43 + }, 44 + "assist": { 45 + "enabled": true, 46 + "actions": { 47 + "source": { 48 + "organizeImports": "on" 49 + } 50 + } 51 + } 52 + }
+20 -3
bun.lock
··· 5 5 "": { 6 6 "name": "purus-ts", 7 7 "devDependencies": { 8 - "@types/bun": "latest", 9 - }, 10 - "peerDependencies": { 8 + "@biomejs/biome": "^2.3.12", 9 + "@types/bun": "^1.1.0", 11 10 "typescript": "^5", 12 11 }, 13 12 }, 14 13 }, 15 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=="], 16 + 17 + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], 18 + 19 + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], 20 + 21 + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="], 22 + 23 + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="], 24 + 25 + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="], 26 + 27 + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="], 28 + 29 + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="], 30 + 31 + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], 32 + 16 33 "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 17 34 18 35 "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+6 -2
package.json
··· 16 16 ], 17 17 "scripts": { 18 18 "build": "bun build src/index.ts --outdir dist --target node && tsc --emitDeclarationOnly", 19 - "type-check": "tsc --noEmit", 20 - "test": "bun test" 19 + "type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", 20 + "lint": "biome check .", 21 + "lint:fix": "biome check --write .", 22 + "format": "biome format --write .", 23 + "test": "tsc --noEmit -p tsconfig.test.json && bun test" 21 24 }, 22 25 "keywords": [ 23 26 "functional-programming", ··· 38 41 }, 39 42 "homepage": "https://tangled.sh/oleksify.me/purus-ts", 40 43 "devDependencies": { 44 + "@biomejs/biome": "^2.3.12", 41 45 "@types/bun": "^1.1.0", 42 46 "typescript": "^5" 43 47 }
+37 -24
src/data/array.ts
··· 1 1 import { pipe } from "../prelude/compose" 2 - import { some, none, type Option } from "../prelude/option" 2 + import { none, type Option, some } from "../prelude/option" 3 3 4 4 // Phantom property type 5 5 declare const __props: unique symbol ··· 19 19 export const arr = <T>(xs: readonly T[]): Arr<T> => xs as Arr<T> 20 20 21 21 // NonEmpty check - returns Option to track the property 22 - export const nonEmpty = <T, P extends string>(xs: Arr<T, P>): Option<Arr<T, P | NonEmpty>> => 22 + export const nonEmpty = <T, P extends string>( 23 + xs: Arr<T, P>, 24 + ): Option<Arr<T, P | NonEmpty>> => 23 25 xs.length > 0 ? some(xs as Arr<T, P | NonEmpty>) : none 24 26 25 27 // Safe accessors for NonEmpty arrays 26 28 export const head = <T, P extends string>(xs: Arr<T, P | NonEmpty>): T => xs[0]! 27 - export const last = <T, P extends string>(xs: Arr<T, P | NonEmpty>): T => xs[xs.length - 1]! 29 + export const last = <T, P extends string>(xs: Arr<T, P | NonEmpty>): T => 30 + xs[xs.length - 1]! 28 31 29 32 // Sorting - adds Sorted property 30 - export const sortNum = <P extends string>(xs: Arr<number, P>): Arr<number, P | Sorted> => 33 + export const sortNum = <P extends string>( 34 + xs: Arr<number, P>, 35 + ): Arr<number, P | Sorted> => 31 36 [...xs].sort((a, b) => a - b) as Arr<number, P | Sorted> 32 37 33 - export const sortBy = <T, P extends string>(key: (t: T) => number) => 38 + export const sortBy = 39 + <T, P extends string>(key: (t: T) => number) => 34 40 (xs: Arr<T, P>): Arr<T, P | Sorted> => 35 41 [...xs].sort((a, b) => key(a) - key(b)) as Arr<T, P | Sorted> 36 42 37 43 // Transformations - preserve properties where valid 38 - export const map = <T, U>(f: (t: T) => U) => 44 + export const map = 45 + <T, U>(f: (t: T) => U) => 39 46 <P extends string>(xs: Arr<T, P>): Arr<U, Exclude<P, Sorted>> => 40 47 xs.map(f) as Arr<U, Exclude<P, Sorted>> 41 48 42 - export const filter = <T>(pred: (t: T) => boolean) => 49 + export const filter = 50 + <T>(pred: (t: T) => boolean) => 43 51 <P extends string>(xs: Arr<T, P>): Arr<T, Exclude<P, NonEmpty>> => 44 52 xs.filter(pred) as Arr<T, Exclude<P, NonEmpty>> 45 53 46 - export const reduce = <T, U>(f: (acc: U, t: T) => U, initial: U) => 54 + export const reduce = 55 + <T, U>(f: (acc: U, t: T) => U, initial: U) => 47 56 <P extends string>(xs: Arr<T, P>): U => 48 57 xs.reduce(f, initial) 49 58 50 59 // Take/drop 51 - export const take = (n: number) => 60 + export const take = 61 + (n: number) => 52 62 <T, P extends string>(xs: Arr<T, P>): Arr<T, Exclude<P, NonEmpty>> => 53 63 xs.slice(0, n) as Arr<T, Exclude<P, NonEmpty>> 54 64 55 - export const drop = (n: number) => 65 + export const drop = 66 + (n: number) => 56 67 <T, P extends string>(xs: Arr<T, P>): Arr<T, Exclude<P, NonEmpty>> => 57 68 xs.slice(n) as Arr<T, Exclude<P, NonEmpty>> 58 69 59 70 // Generic binary search with comparator 60 - export const binarySearch = <T>(compare: (a: T, b: T) => number) => 71 + export const binarySearch = 72 + <T>(compare: (a: T, b: T) => number) => 61 73 (target: T) => 62 - <P extends string>(xs: Arr<T, P | Sorted>): Option<number> => { 63 - const go = (lo: number, hi: number): Option<number> => 64 - lo > hi 65 - ? none 66 - : pipe( 67 - Math.floor((lo + hi) / 2), 68 - mid => ( 69 - (cmp => cmp === 0 ? some(mid) : cmp < 0 ? go(lo, mid - 1) : go(mid + 1, hi)) 70 - (compare(target, xs[mid]!)) 71 - ) 72 - ) 73 - return go(0, xs.length - 1) 74 - } 74 + <P extends string>(xs: Arr<T, P | Sorted>): Option<number> => { 75 + const go = (lo: number, hi: number): Option<number> => 76 + lo > hi 77 + ? none 78 + : pipe(Math.floor((lo + hi) / 2), (mid) => 79 + ((cmp) => 80 + cmp === 0 81 + ? some(mid) 82 + : cmp < 0 83 + ? go(lo, mid - 1) 84 + : go(mid + 1, hi))(compare(target, xs[mid]!)), 85 + ) 86 + return go(0, xs.length - 1) 87 + } 75 88 76 89 // Binary search specialized for numbers 77 90 export const binarySearchNum = binarySearch<number>((a, b) => a - b)
+20 -15
src/data/guards.ts
··· 18 18 export const isBoolean = (x: unknown): x is boolean => typeof x === "boolean" 19 19 20 20 /** Type guard for non-null object values */ 21 - export const isObject = (x: unknown): x is object => typeof x === "object" && x !== null 21 + export const isObject = (x: unknown): x is object => 22 + typeof x === "object" && x !== null 22 23 23 24 /** Type guard for array values */ 24 25 export const isArray = (x: unknown): x is unknown[] => Array.isArray(x) ··· 34 35 export const isNegative = (x: unknown): x is number => isNumber(x) && x < 0 35 36 36 37 /** Type guard for integer numbers */ 37 - export const isInteger = (x: unknown): x is number => isNumber(x) && Number.isInteger(x) 38 + export const isInteger = (x: unknown): x is number => 39 + isNumber(x) && Number.isInteger(x) 38 40 39 41 /** Type guard for finite numbers (excludes Infinity and NaN) */ 40 - export const isFiniteNumber = (x: unknown): x is number => isNumber(x) && Number.isFinite(x) 42 + export const isFiniteNumber = (x: unknown): x is number => 43 + isNumber(x) && Number.isFinite(x) 41 44 42 45 // ============================================================================= 43 46 // Guard Combinators 44 47 // ============================================================================= 45 48 46 49 /** Combines two guards with logical AND - both must pass */ 47 - export const and = <A, B extends A, C extends B>( 48 - g1: (x: A) => x is B, 49 - g2: (x: B) => x is C 50 - ) => (x: A): x is C => g1(x) && g2(x) 50 + export const and = 51 + <A, B extends A, C extends B>(g1: (x: A) => x is B, g2: (x: B) => x is C) => 52 + (x: A): x is C => 53 + g1(x) && g2(x) 51 54 52 55 /** Combines two guards with logical OR - either must pass */ 53 - export const or = <A, B extends A, C extends A>( 54 - g1: (x: A) => x is B, 55 - g2: (x: A) => x is C 56 - ) => (x: A): x is B | C => g1(x) || g2(x) 56 + export const or = 57 + <A, B extends A, C extends A>(g1: (x: A) => x is B, g2: (x: A) => x is C) => 58 + (x: A): x is B | C => 59 + g1(x) || g2(x) 57 60 58 61 /** Negates a guard */ 59 - export const not = <A, B extends A>( 60 - guard: (x: A) => x is B 61 - ) => (x: A): x is Exclude<A, B> => !guard(x) 62 + export const not = 63 + <A, B extends A>(guard: (x: A) => x is B) => 64 + (x: A): x is Exclude<A, B> => 65 + !guard(x) 62 66 63 67 // ============================================================================= 64 68 // Property Guards 65 69 // ============================================================================= 66 70 67 71 /** Creates a guard that checks if an object has a specific property */ 68 - export const hasProperty = <K extends string>(key: K) => 72 + export const hasProperty = 73 + <K extends string>(key: K) => 69 74 <T>(x: T): x is T & Record<K, unknown> => 70 75 isObject(x) && key in x
+2 -2
src/data/index.ts
··· 4 4 * @module data 5 5 */ 6 6 7 - export * from "./guards" 8 7 export * from "./array" 9 - export * from "./units" 10 8 export * from "./entity" 9 + export * from "./guards" 10 + export * from "./units" 11 11 export * from "./validation"
+6 -5
src/data/units.ts
··· 70 70 * const pixels = (n: number): Quantity<number, Pixels> => quantity<Pixels>(n) 71 71 * ``` 72 72 */ 73 - export const quantity = <U extends string>(value: number): Quantity<number, U> => 74 - value as Quantity<number, U> 73 + export const quantity = <U extends string>( 74 + value: number, 75 + ): Quantity<number, U> => value as Quantity<number, U> 75 76 76 77 /** 77 78 * Create a quantity in meters. ··· 120 121 */ 121 122 export const velocity = ( 122 123 distance: Quantity<number, Meters>, 123 - time: Quantity<number, Seconds> 124 + time: Quantity<number, Seconds>, 124 125 ): Quantity<number, MetersPerSecond> => 125 126 (unquantity(distance) / unquantity(time)) as Quantity<number, MetersPerSecond> 126 127 ··· 138 139 */ 139 140 export const addQ = <U>( 140 141 a: Quantity<number, U>, 141 - b: Quantity<number, U> 142 + b: Quantity<number, U>, 142 143 ): Quantity<number, U> => (unquantity(a) + unquantity(b)) as Quantity<number, U> 143 144 144 145 /** ··· 151 152 */ 152 153 export const subQ = <U>( 153 154 a: Quantity<number, U>, 154 - b: Quantity<number, U> 155 + b: Quantity<number, U>, 155 156 ): Quantity<number, U> => (unquantity(a) - unquantity(b)) as Quantity<number, U> 156 157 157 158 /**
+31 -16
src/data/validation.ts
··· 24 24 * @module data/validation 25 25 */ 26 26 27 - import { ok, err, type Result } from "../prelude/result" 27 + import { err, ok, type Result } from "../prelude/result" 28 28 29 29 // ----------------------------------------------------------------------------- 30 30 // Types ··· 38 38 /** 39 39 * Invalid variant of Validation. Contains accumulated errors. 40 40 */ 41 - export type Invalid<E> = { readonly _tag: "Invalid"; readonly errors: readonly E[] } 41 + export type Invalid<E> = { 42 + readonly _tag: "Invalid" 43 + readonly errors: readonly E[] 44 + } 42 45 43 46 /** 44 47 * Validation<A, E> - Either a valid value A or accumulated errors E[]. ··· 75 78 * // Type: Invalid<string> 76 79 * ``` 77 80 */ 78 - export const invalid = <E>(errors: readonly E[]): Invalid<E> => ({ _tag: "Invalid", errors }) 81 + export const invalid = <E>(errors: readonly E[]): Invalid<E> => ({ 82 + _tag: "Invalid", 83 + errors, 84 + }) 79 85 80 86 /** 81 87 * Create an invalid Validation from a single error. ··· 86 92 * // Type: Invalid<string> 87 93 * ``` 88 94 */ 89 - export const invalidOne = <E>(error: E): Invalid<E> => ({ _tag: "Invalid", errors: [error] }) 95 + export const invalidOne = <E>(error: E): Invalid<E> => ({ 96 + _tag: "Invalid", 97 + errors: [error], 98 + }) 90 99 91 100 // ----------------------------------------------------------------------------- 92 101 // Type Guards ··· 95 104 /** 96 105 * Type guard to check if a Validation is valid. 97 106 */ 98 - export const isValid = <A, E>(v: Validation<A, E>): v is Valid<A> => v._tag === "Valid" 107 + export const isValid = <A, E>(v: Validation<A, E>): v is Valid<A> => 108 + v._tag === "Valid" 99 109 100 110 /** 101 111 * Type guard to check if a Validation is invalid. 102 112 */ 103 - export const isInvalid = <A, E>(v: Validation<A, E>): v is Invalid<E> => v._tag === "Invalid" 113 + export const isInvalid = <A, E>(v: Validation<A, E>): v is Invalid<E> => 114 + v._tag === "Invalid" 104 115 105 116 // ----------------------------------------------------------------------------- 106 117 // Functor ··· 116 127 * pipe(invalid(["oops"]), mapValidation(n => n * 2)) // Invalid(["oops"]) 117 128 * ``` 118 129 */ 119 - export const mapValidation = <A, B, E>(f: (a: A) => B) => 130 + export const mapValidation = 131 + <A, B, E>(f: (a: A) => B) => 120 132 (v: Validation<A, E>): Validation<B, E> => 121 133 v._tag === "Valid" ? valid(f(v.value)) : v 122 134 ··· 151 163 * ) // Invalid(["error1", "error2"]) 152 164 * ``` 153 165 */ 154 - export const apValidation = <A, B, E>(va: Validation<A, E>) => 166 + export const apValidation = 167 + <A, B, E>(va: Validation<A, E>) => 155 168 (vf: Validation<(a: A) => B, E>): Validation<B, E> => 156 169 vf._tag === "Valid" && va._tag === "Valid" 157 170 ? valid(vf.value(va.value)) 158 171 : vf._tag === "Invalid" && va._tag === "Invalid" 159 172 ? invalid([...vf.errors, ...va.errors]) 160 - : vf._tag === "Invalid" ? vf : va as Invalid<E> 173 + : vf._tag === "Invalid" 174 + ? vf 175 + : (va as Invalid<E>) 161 176 162 177 // ----------------------------------------------------------------------------- 163 178 // Conversions ··· 198 213 * toResultAll(invalid(["err1", "err2"])) // Err(["err1", "err2"]) 199 214 * ``` 200 215 */ 201 - export const toResultAll = <A, E>(v: Validation<A, E>): Result<A, readonly E[]> => 202 - v._tag === "Valid" ? ok(v.value) : err(v.errors) 216 + export const toResultAll = <A, E>( 217 + v: Validation<A, E>, 218 + ): Result<A, readonly E[]> => (v._tag === "Valid" ? ok(v.value) : err(v.errors)) 203 219 204 220 // ----------------------------------------------------------------------------- 205 221 // Pattern Matching ··· 216 232 * )(validation) 217 233 * ``` 218 234 */ 219 - export const matchValidation = <A, E, R>( 220 - onValid: (value: A) => R, 221 - onInvalid: (errors: readonly E[]) => R 222 - ) => (v: Validation<A, E>): R => 223 - v._tag === "Valid" ? onValid(v.value) : onInvalid(v.errors) 235 + export const matchValidation = 236 + <A, E, R>(onValid: (value: A) => R, onInvalid: (errors: readonly E[]) => R) => 237 + (v: Validation<A, E>): R => 238 + v._tag === "Valid" ? onValid(v.value) : onInvalid(v.errors) 224 239 225 240 // ----------------------------------------------------------------------------- 226 241 // Utilities
+128 -92
src/effect/combinators.ts
··· 1 1 import { pipe } from "../prelude/compose" 2 - import { some, none, matchOption, type Option } from "../prelude/option" 3 - import { succeed, async, flatMap, mapEff, fork, foldEff, type Eff, type Fiber } from "./eff" 4 - import { success, failure, interrupted, type Exit } from "./exit" 2 + import { matchOption, none, type Option, some } from "../prelude/option" 3 + import { 4 + async, 5 + type Eff, 6 + type Fiber, 7 + flatMap, 8 + foldEff, 9 + fork, 10 + mapEff, 11 + succeed, 12 + } from "./eff" 13 + import { type Exit, failure, interrupted, success } from "./exit" 5 14 6 15 // === TapEff - side effect that returns original value === 7 16 8 - export const tapEff = <A>(f: (a: A) => void) => 17 + export const tapEff = 18 + <A>(f: (a: A) => void) => 9 19 <E, R>(eff: Eff<A, E, R>): Eff<A, E, R> => 10 20 pipe( 11 21 eff, 12 22 flatMap((a: A) => { 13 23 f(a) 14 24 return succeed(a) as Eff<A, E, R> 15 - }) 25 + }), 16 26 ) 17 27 18 28 // === Join === 19 29 20 30 export const join = <A, E>(fiber: Fiber<A, E>): Eff<A, E, unknown> => 21 - async(resume => { 31 + async((resume) => { 22 32 fiber.await().then(resume) 23 33 return () => {} 24 34 }) 25 35 26 36 // === Interrupt === 27 37 28 - export const interruptFiber = <A, E>(fiber: Fiber<A, E>): Eff<void, never, unknown> => 29 - async(resume => { 38 + export const interruptFiber = <A, E>( 39 + fiber: Fiber<A, E>, 40 + ): Eff<void, never, unknown> => 41 + async((resume) => { 30 42 fiber.interrupt() 31 43 resume(success(undefined)) 32 44 return () => {} ··· 36 48 37 49 export const race = <A, E, R>( 38 50 left: Eff<A, E, R>, 39 - right: Eff<A, E, R> 51 + right: Eff<A, E, R>, 40 52 ): Eff<A, E, R> => { 41 53 // Fork both effects and wait for first to complete 42 54 const forkedLeft = fork(left) as unknown as Eff<Fiber<A, E>, E, R> ··· 47 59 flatMap((leftFiber: Fiber<A, E>) => 48 60 pipe( 49 61 forkedRight, 50 - flatMap((rightFiber: Fiber<A, E>) => 51 - async<A, E>(resume => { 52 - let done = false 62 + flatMap( 63 + (rightFiber: Fiber<A, E>) => 64 + async<A, E>((resume) => { 65 + let done = false 53 66 54 - const finish = (exit: Exit<A, E>, loser: Fiber<A, E>) => { 55 - if (done) return 56 - done = true 57 - loser.interrupt() 58 - resume(exit) 59 - } 67 + const finish = (exit: Exit<A, E>, loser: Fiber<A, E>) => { 68 + if (done) return 69 + done = true 70 + loser.interrupt() 71 + resume(exit) 72 + } 60 73 61 - leftFiber.await().then(exit => finish(exit, rightFiber)) 62 - rightFiber.await().then(exit => finish(exit, leftFiber)) 74 + leftFiber.await().then((exit) => finish(exit, rightFiber)) 75 + rightFiber.await().then((exit) => finish(exit, leftFiber)) 63 76 64 - return () => { 65 - leftFiber.interrupt() 66 - rightFiber.interrupt() 67 - } 68 - }) as Eff<A, E, R> 69 - ) 70 - ) 71 - ) 77 + return () => { 78 + leftFiber.interrupt() 79 + rightFiber.interrupt() 80 + } 81 + }) as Eff<A, E, R>, 82 + ), 83 + ), 84 + ), 72 85 ) 73 86 } 74 87 75 88 // === All - run effects in parallel, collect results === 76 89 77 - export const all = <A, E, R>(effs: readonly Eff<A, E, R>[]): Eff<readonly A[], E, R> => { 90 + export const all = <A, E, R>( 91 + effs: readonly Eff<A, E, R>[], 92 + ): Eff<readonly A[], E, R> => { 78 93 if (effs.length === 0) { 79 94 return succeed([]) 80 95 } ··· 87 102 flatMap((fibers: Fiber<A, E>[]) => 88 103 pipe( 89 104 fork(eff) as unknown as Eff<Fiber<A, E>, E, R>, 90 - mapEff((fiber: Fiber<A, E>) => [...fibers, fiber]) 91 - ) 92 - ) 105 + mapEff((fiber: Fiber<A, E>) => [...fibers, fiber]), 106 + ), 107 + ), 93 108 ), 94 - succeed([]) as Eff<Fiber<A, E>[], E, R> 109 + succeed([]) as Eff<Fiber<A, E>[], E, R>, 95 110 ) 96 111 97 112 return pipe( 98 113 forkAll, 99 - flatMap((fibers: Fiber<A, E>[]) => 100 - async<readonly A[], E>(resume => { 101 - const results: A[] = new Array(fibers.length) 102 - let completed = 0 103 - let done = false 114 + flatMap( 115 + (fibers: Fiber<A, E>[]) => 116 + async<readonly A[], E>((resume) => { 117 + const results: A[] = new Array(fibers.length) 118 + let completed = 0 119 + let done = false 104 120 105 - const interruptAll = () => { 106 - fibers.forEach(f => f.interrupt()) 107 - } 121 + const interruptAll = () => { 122 + fibers.forEach((f) => f.interrupt()) 123 + } 108 124 109 - fibers.forEach((fiber, index) => { 110 - fiber.await().then(exit => { 111 - if (done) return 125 + fibers.forEach((fiber, index) => { 126 + fiber.await().then((exit) => { 127 + if (done) return 112 128 113 - if (exit._tag === "Success") { 114 - results[index] = exit.value 115 - completed++ 116 - if (completed === fibers.length) { 129 + if (exit._tag === "Success") { 130 + results[index] = exit.value 131 + completed++ 132 + if (completed === fibers.length) { 133 + done = true 134 + resume(success(results)) 135 + } 136 + } else if (exit._tag === "Failure") { 117 137 done = true 118 - resume(success(results)) 138 + interruptAll() 139 + resume(failure(exit.error)) 140 + } else { 141 + // Interrupted - propagate interruption 142 + done = true 143 + interruptAll() 144 + resume(interrupted(exit.by)) 119 145 } 120 - } else if (exit._tag === "Failure") { 121 - done = true 122 - interruptAll() 123 - resume(failure(exit.error)) 124 - } else { 125 - // Interrupted - propagate interruption 126 - done = true 127 - interruptAll() 128 - resume(interrupted(exit.by)) 129 - } 146 + }) 130 147 }) 131 - }) 132 148 133 - return interruptAll 134 - }) as Eff<readonly A[], E, R> 135 - ) 149 + return interruptAll 150 + }) as Eff<readonly A[], E, R>, 151 + ), 136 152 ) 137 153 } 138 154 139 155 // Simpler sequential all that works correctly 140 - export const allSequential = <A, E, R>(effs: readonly Eff<A, E, R>[]): Eff<readonly A[], E, R> => 156 + export const allSequential = <A, E, R>( 157 + effs: readonly Eff<A, E, R>[], 158 + ): Eff<readonly A[], E, R> => 141 159 effs.length === 0 142 160 ? succeed([]) 143 161 : pipe( 144 162 effs[0]!, 145 - flatMap(first => 163 + flatMap((first) => 146 164 pipe( 147 165 allSequential(effs.slice(1)), 148 - mapEff(rest => [first, ...rest] as readonly A[]) 149 - ) 150 - ) 166 + mapEff((rest) => [first, ...rest] as readonly A[]), 167 + ), 168 + ), 151 169 ) 152 170 153 171 // === Traverse / Sequence === ··· 166 184 * ) // Eff<readonly User[], Error, HttpClient> 167 185 * ``` 168 186 */ 169 - export const traverse = <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 187 + export const traverse = 188 + <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 170 189 (as: readonly A[]): Eff<readonly B[], E, R> => 171 190 as.length === 0 172 191 ? succeed([]) 173 192 : pipe( 174 193 f(as[0]!), 175 - flatMap(b => pipe( 176 - traverse(f)(as.slice(1)), 177 - mapEff(bs => [b, ...bs] as readonly B[]) 178 - )) 194 + flatMap((b) => 195 + pipe( 196 + traverse(f)(as.slice(1)), 197 + mapEff((bs) => [b, ...bs] as readonly B[]), 198 + ), 199 + ), 179 200 ) 180 201 181 202 /** ··· 189 210 * ``` 190 211 */ 191 212 export const sequence = <A, E, R>( 192 - effs: readonly Eff<A, E, R>[] 193 - ): Eff<readonly A[], E, R> => 194 - traverse((eff: Eff<A, E, R>) => eff)(effs) 213 + effs: readonly Eff<A, E, R>[], 214 + ): Eff<readonly A[], E, R> => traverse((eff: Eff<A, E, R>) => eff)(effs) 195 215 196 216 /** 197 217 * Like traverse but runs all effects in parallel. ··· 205 225 * ) // All fetches run concurrently 206 226 * ``` 207 227 */ 208 - export const traversePar = <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 209 - (as: readonly A[]): Eff<readonly B[], E, R> => all(as.map(f)) 228 + export const traversePar = 229 + <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 230 + (as: readonly A[]): Eff<readonly B[], E, R> => 231 + all(as.map(f)) 210 232 211 233 /** 212 234 * Like sequence but runs all effects in parallel. ··· 222 244 223 245 // === Timeout === 224 246 225 - export const timeout = (ms: number) => 247 + export const timeout = 248 + (ms: number) => 226 249 <A, E, R>(eff: Eff<A, E, R>): Eff<A | null, E, R> => 227 250 pipe( 228 251 race( 229 - pipe(eff, mapEff(a => some(a))), 230 - async<Option<A>, E>(resume => { 252 + pipe( 253 + eff, 254 + mapEff((a) => some(a)), 255 + ), 256 + async<Option<A>, E>((resume) => { 231 257 const id = setTimeout(() => resume(success(none)), ms) 232 258 return () => clearTimeout(id) 233 - }) 259 + }), 260 + ), 261 + mapEff((opt) => 262 + matchOption( 263 + (a: A) => a, 264 + () => null as A | null, 265 + )(opt), 234 266 ), 235 - mapEff(opt => matchOption( 236 - (a: A) => a, 237 - () => null as A | null 238 - )(opt)) 239 267 ) 240 268 241 269 // === Retry === 242 270 243 - export const retry = (times: number) => 271 + export const retry = 272 + (times: number) => 244 273 <A, E, R>(eff: Eff<A, E, R>): Eff<A, E, R> => { 245 274 const go = (remaining: number): Eff<A, E, R> => 246 275 remaining <= 0 ··· 249 278 eff, 250 279 foldEff( 251 280 (_e: E) => go(remaining - 1), 252 - (a: A) => succeed(a) as Eff<A, E, R> 253 - ) 281 + (a: A) => succeed(a) as Eff<A, E, R>, 282 + ), 254 283 ) 255 284 256 285 return go(times) ··· 258 287 259 288 // === Repeat === 260 289 261 - export const repeatEff = (times: number) => 290 + export const repeatEff = 291 + (times: number) => 262 292 <A, E, R>(eff: Eff<A, E, R>): Eff<A, E, R> => { 263 293 const go = (remaining: number, lastValue: A): Eff<A, E, R> => 264 294 remaining <= 0 265 295 ? succeed(lastValue) 266 - : pipe(eff, flatMap(a => go(remaining - 1, a))) 296 + : pipe( 297 + eff, 298 + flatMap((a) => go(remaining - 1, a)), 299 + ) 267 300 268 301 return times <= 0 269 302 ? eff 270 - : pipe(eff, flatMap(a => go(times - 1, a))) 303 + : pipe( 304 + eff, 305 + flatMap((a) => go(times - 1, a)), 306 + ) 271 307 }
+84 -43
src/effect/eff.ts
··· 46 46 | { readonly _tag: "Succeed"; readonly value: A } 47 47 | { readonly _tag: "Fail"; readonly error: E } 48 48 | { readonly _tag: "Sync"; readonly f: () => A } 49 - | { readonly _tag: "Async"; readonly register: (resume: (exit: Exit<A, E>) => void) => Cleanup } 50 - | { readonly _tag: "FlatMap"; readonly effect: Eff<unknown, E, R>; readonly f: (a: unknown) => Eff<A, E, R> } 51 - | { readonly _tag: "Fold"; readonly effect: Eff<unknown, unknown, R>; readonly onErr: (e: unknown) => Eff<A, E, R>; readonly onSucc: (a: unknown) => Eff<A, E, R> } 49 + | { 50 + readonly _tag: "Async" 51 + readonly register: (resume: (exit: Exit<A, E>) => void) => Cleanup 52 + } 53 + | { 54 + readonly _tag: "FlatMap" 55 + readonly effect: Eff<unknown, E, R> 56 + readonly f: (a: unknown) => Eff<A, E, R> 57 + } 58 + | { 59 + readonly _tag: "Fold" 60 + readonly effect: Eff<unknown, unknown, R> 61 + readonly onErr: (e: unknown) => Eff<A, E, R> 62 + readonly onSucc: (a: unknown) => Eff<A, E, R> 63 + } 52 64 | { readonly _tag: "Access"; readonly f: (r: R) => A } 53 65 | { readonly _tag: "AccessEff"; readonly f: (r: R) => Eff<A, E, R> } 54 - | { readonly _tag: "Provide"; readonly effect: Eff<A, E, unknown>; readonly env: unknown } 66 + | { 67 + readonly _tag: "Provide" 68 + readonly effect: Eff<A, E, unknown> 69 + readonly env: unknown 70 + } 55 71 | { readonly _tag: "Fork"; readonly effect: Eff<A, E, R> } 56 72 | { readonly _tag: "YieldNow" } 57 73 | { readonly _tag: "GetFiberId" } ··· 72 88 * await runPromise(eff) // 42 73 89 * ``` 74 90 */ 75 - export const succeed = <A>(value: A): Eff<A, never, unknown> => 76 - ({ _tag: "Succeed", value }) 91 + export const succeed = <A>(value: A): Eff<A, never, unknown> => ({ 92 + _tag: "Succeed", 93 + value, 94 + }) 77 95 78 96 /** 79 97 * Create an effect that fails with an error. ··· 84 102 * await runPromise(eff) // throws { _tag: "NotFound", id: "123" } 85 103 * ``` 86 104 */ 87 - export const fail = <E>(error: E): Eff<never, E, unknown> => 88 - ({ _tag: "Fail", error }) 105 + export const fail = <E>(error: E): Eff<never, E, unknown> => ({ 106 + _tag: "Fail", 107 + error, 108 + }) 89 109 90 110 /** 91 111 * Create an effect from a synchronous function. ··· 98 118 * await runPromise(now) // New current date 99 119 * ``` 100 120 */ 101 - export const sync = <A>(f: () => A): Eff<A, never, unknown> => 102 - ({ _tag: "Sync", f }) 121 + export const sync = <A>(f: () => A): Eff<A, never, unknown> => ({ 122 + _tag: "Sync", 123 + f, 124 + }) 103 125 104 126 /** 105 127 * ESCAPE HATCH: Bridges callback-based async APIs into the effect system. ··· 132 154 * ``` 133 155 */ 134 156 export const async = <A, E>( 135 - register: (resume: (exit: Exit<A, E>) => void) => Cleanup 136 - ): Eff<A, E, unknown> => 137 - ({ _tag: "Async", register }) 157 + register: (resume: (exit: Exit<A, E>) => void) => Cleanup, 158 + ): Eff<A, E, unknown> => ({ _tag: "Async", register }) 138 159 139 160 /** 140 161 * Yield control to other fibers. 141 162 * Useful for cooperative multitasking in long-running computations. 142 163 */ 143 - export const yieldNow: Eff<void, never, unknown> = 144 - ({ _tag: "YieldNow" }) 164 + export const yieldNow: Eff<void, never, unknown> = { _tag: "YieldNow" } 145 165 146 166 // FiberId counter and constructor 147 167 let fiberIdCounter = 0 148 - export const makeFiberId = (): FiberId => ({ _tag: "FiberId", id: ++fiberIdCounter }) 168 + export const makeFiberId = (): FiberId => ({ 169 + _tag: "FiberId", 170 + id: ++fiberIdCounter, 171 + }) 149 172 150 173 /** 151 174 * Get the current fiber's ID. 152 175 */ 153 - export const fiberId: Eff<FiberId, never, unknown> = 154 - ({ _tag: "GetFiberId" }) 176 + export const fiberId: Eff<FiberId, never, unknown> = { _tag: "GetFiberId" } 155 177 156 178 // === Transformations === 157 179 ··· 176 198 * // Eff<string, never, unknown> 177 199 * ``` 178 200 */ 179 - export const flatMap = <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 180 - (effect: Eff<A, E, R>): Eff<B, E, R> => 181 - ({ _tag: "FlatMap", effect: effect as Eff<unknown, E, R>, f: f as (a: unknown) => Eff<B, E, R> }) 201 + export const flatMap = 202 + <A, B, E, R>(f: (a: A) => Eff<B, E, R>) => 203 + (effect: Eff<A, E, R>): Eff<B, E, R> => ({ 204 + _tag: "FlatMap", 205 + effect: effect as Eff<unknown, E, R>, 206 + f: f as (a: unknown) => Eff<B, E, R>, 207 + }) 182 208 183 209 /** 184 210 * Transform the success value of an effect. ··· 193 219 * // Eff<number, never, unknown> with value 20 194 220 * ``` 195 221 */ 196 - export const mapEff = <A, B, E, R>(f: (a: A) => B) => 222 + export const mapEff = 223 + <A, B, E, R>(f: (a: A) => B) => 197 224 (effect: Eff<A, E, R>): Eff<B, E, R> => 198 225 flatMap((a: A) => succeed(f(a)) as Eff<B, E, R>)(effect) 199 226 ··· 211 238 * ) 212 239 * ``` 213 240 */ 214 - export const foldEff = <A, B, E, E2, R>( 215 - onErr: (e: E) => Eff<B, E2, R>, 216 - onSucc: (a: A) => Eff<B, E2, R> 217 - ) => (effect: Eff<A, E, R>): Eff<B, E2, R> => 218 - ({ 241 + export const foldEff = 242 + <A, B, E, E2, R>( 243 + onErr: (e: E) => Eff<B, E2, R>, 244 + onSucc: (a: A) => Eff<B, E2, R>, 245 + ) => 246 + (effect: Eff<A, E, R>): Eff<B, E2, R> => ({ 219 247 _tag: "Fold", 220 248 effect: effect as Eff<unknown, unknown, R>, 221 249 onErr: onErr as (e: unknown) => Eff<B, E2, R>, 222 - onSucc: onSucc as (a: unknown) => Eff<B, E2, R> 250 + onSucc: onSucc as (a: unknown) => Eff<B, E2, R>, 223 251 }) 224 252 225 253 /** ··· 233 261 * ) 234 262 * ``` 235 263 */ 236 - export const catchAll = <A, E, E2, R>(onErr: (e: E) => Eff<A, E2, R>) => 264 + export const catchAll = 265 + <A, E, E2, R>(onErr: (e: E) => Eff<A, E2, R>) => 237 266 (effect: Eff<A, E, R>): Eff<A, E2, R> => 238 267 foldEff(onErr, (a: A) => succeed(a))(effect) 239 268 ··· 250 279 * // Eff<string, never, Config> 251 280 * ``` 252 281 */ 253 - export const access = <R, A>(f: (r: R) => A): Eff<A, never, R> => 254 - ({ _tag: "Access", f }) 282 + export const access = <R, A>(f: (r: R) => A): Eff<A, never, R> => ({ 283 + _tag: "Access", 284 + f, 285 + }) 255 286 256 287 /** 257 288 * Access the environment and run an effect with it. ··· 267 298 * }) 268 299 * ``` 269 300 */ 270 - export const accessEff = <R, A, E>(f: (r: R) => Eff<A, E, R>): Eff<A, E, R> => 271 - ({ _tag: "AccessEff", f }) 301 + export const accessEff = <R, A, E>( 302 + f: (r: R) => Eff<A, E, R>, 303 + ): Eff<A, E, R> => ({ _tag: "AccessEff", f }) 272 304 273 305 /** 274 306 * Provide an environment to an effect, removing the requirement. ··· 290 322 * // Eff<User[], DbError, unknown> 291 323 * ``` 292 324 */ 293 - export const provide = <R>(env: R) => 294 - <A, E>(effect: Eff<A, E, R>): Eff<A, E, unknown> => 295 - ({ _tag: "Provide", effect: effect as Eff<A, E, unknown>, env }) 325 + export const provide = 326 + <R>(env: R) => 327 + <A, E>(effect: Eff<A, E, R>): Eff<A, E, unknown> => ({ 328 + _tag: "Provide", 329 + effect: effect as Eff<A, E, unknown>, 330 + env, 331 + }) 296 332 297 333 // === Concurrency === 298 334 ··· 307 343 * const result = await fiber.await() 308 344 * ``` 309 345 */ 310 - export const fork = <A, E, R>(effect: Eff<A, E, R>): Eff<Fiber<A, E>, never, R> => 346 + export const fork = <A, E, R>( 347 + effect: Eff<A, E, R>, 348 + ): Eff<Fiber<A, E>, never, R> => 311 349 ({ _tag: "Fork", effect }) as unknown as Eff<Fiber<A, E>, never, R> 312 350 313 351 /** ··· 339 377 * ``` 340 378 */ 341 379 export const attempt = <A>(f: () => A): Eff<A, unknown, unknown> => 342 - async(resume => { 380 + async((resume) => { 343 381 try { 344 382 resume({ _tag: "Success", value: f() }) 345 383 } catch (e) { ··· 359 397 * ``` 360 398 */ 361 399 export const fromPromise = <A>(f: () => Promise<A>): Eff<A, unknown, unknown> => 362 - async(resume => { 400 + async((resume) => { 363 401 f() 364 - .then(value => resume({ _tag: "Success", value })) 365 - .catch(error => resume({ _tag: "Failure", error })) 402 + .then((value) => resume({ _tag: "Success", value })) 403 + .catch((error) => resume({ _tag: "Failure", error })) 366 404 return () => {} 367 405 }) 368 406 ··· 379 417 * ``` 380 418 */ 381 419 export const sleep = (ms: number): Eff<void, never, unknown> => 382 - async(resume => { 383 - const id = setTimeout(() => resume({ _tag: "Success", value: undefined }), ms) 420 + async((resume) => { 421 + const id = setTimeout( 422 + () => resume({ _tag: "Success", value: undefined }), 423 + ms, 424 + ) 384 425 return () => clearTimeout(id) 385 426 })
+42 -29
src/effect/exit.ts
··· 10 10 | { readonly _tag: "Interrupted"; readonly by: string } 11 11 12 12 // Constructors 13 - export const success = <A>(value: A): Exit<A, never> => 14 - ({ _tag: "Success", value }) 13 + export const success = <A>(value: A): Exit<A, never> => ({ 14 + _tag: "Success", 15 + value, 16 + }) 15 17 16 - export const failure = <E>(error: E): Exit<never, E> => 17 - ({ _tag: "Failure", error }) 18 + export const failure = <E>(error: E): Exit<never, E> => ({ 19 + _tag: "Failure", 20 + error, 21 + }) 18 22 19 - export const interrupted = (by: string): Exit<never, never> => 20 - ({ _tag: "Interrupted", by }) 23 + export const interrupted = (by: string): Exit<never, never> => ({ 24 + _tag: "Interrupted", 25 + by, 26 + }) 21 27 22 28 // Type guards 23 - export const isSuccess = <A, E>(exit: Exit<A, E>): exit is Extract<Exit<A, E>, { _tag: "Success" }> => 24 - exit._tag === "Success" 29 + export const isSuccess = <A, E>( 30 + exit: Exit<A, E>, 31 + ): exit is Extract<Exit<A, E>, { _tag: "Success" }> => exit._tag === "Success" 25 32 26 - export const isFailure = <A, E>(exit: Exit<A, E>): exit is Extract<Exit<A, E>, { _tag: "Failure" }> => 27 - exit._tag === "Failure" 33 + export const isFailure = <A, E>( 34 + exit: Exit<A, E>, 35 + ): exit is Extract<Exit<A, E>, { _tag: "Failure" }> => exit._tag === "Failure" 28 36 29 - export const isInterrupted = <A, E>(exit: Exit<A, E>): exit is Extract<Exit<A, E>, { _tag: "Interrupted" }> => 37 + export const isInterrupted = <A, E>( 38 + exit: Exit<A, E>, 39 + ): exit is Extract<Exit<A, E>, { _tag: "Interrupted" }> => 30 40 exit._tag === "Interrupted" 31 41 32 42 // Match helper 33 - export const matchExit = <A, E, R>( 34 - onSuccess: (value: A) => R, 35 - onFailure: (error: E) => R, 36 - onInterrupted: (by: string) => R 37 - ) => (exit: Exit<A, E>): R => 38 - match(exit)({ 39 - Success: ({ value }) => onSuccess(value), 40 - Failure: ({ error }) => onFailure(error), 41 - Interrupted: ({ by }) => onInterrupted(by) 42 - }) 43 + export const matchExit = 44 + <A, E, R>( 45 + onSuccess: (value: A) => R, 46 + onFailure: (error: E) => R, 47 + onInterrupted: (by: string) => R, 48 + ) => 49 + (exit: Exit<A, E>): R => 50 + match(exit)({ 51 + Success: ({ value }) => onSuccess(value), 52 + Failure: ({ error }) => onFailure(error), 53 + Interrupted: ({ by }) => onInterrupted(by), 54 + }) 43 55 44 56 /** 45 57 * Transform both success and failure values of an Exit (Bifunctor). ··· 56 68 * ) // Success(20) 57 69 * ``` 58 70 */ 59 - export const bimapExit = <A, B, E, F>( 60 - onSuccess: (a: A) => B, 61 - onFailure: (e: E) => F 62 - ) => (exit: Exit<A, E>): Exit<B, F> => 63 - exit._tag === "Success" ? success(onSuccess(exit.value)) : 64 - exit._tag === "Failure" ? failure(onFailure(exit.error)) : 65 - interrupted(exit.by) 71 + export const bimapExit = 72 + <A, B, E, F>(onSuccess: (a: A) => B, onFailure: (e: E) => F) => 73 + (exit: Exit<A, E>): Exit<B, F> => 74 + exit._tag === "Success" 75 + ? success(onSuccess(exit.value)) 76 + : exit._tag === "Failure" 77 + ? failure(onFailure(exit.error)) 78 + : interrupted(exit.by) 66 79 67 80 // Namespace for convenient access (using original naming convention) 68 81 export const Exit = { ··· 76 89 isFailure, 77 90 isInterrupted, 78 91 match: matchExit, 79 - bimap: bimapExit 92 + bimap: bimapExit, 80 93 }
+3 -3
src/effect/index.ts
··· 1 - export * from "./exit" 1 + export * from "./combinators" 2 2 export * from "./eff" 3 - export * from "./step" 3 + export * from "./exit" 4 4 export * from "./interpret" 5 + export * from "./step" 5 6 export * from "./trampoline" 6 - export * from "./combinators"
+57 -37
src/effect/interpret.ts
··· 1 1 import { match } from "../prelude/match" 2 2 import type { Eff, Fiber, FiberId } from "./eff" 3 3 import { makeFiberId } from "./eff" 4 - import { matchExit, type Exit } from "./exit" 5 - import { suspended, blocked, type Step } from "./step" 4 + import { type Exit, matchExit } from "./exit" 5 + import { blocked, type Step, suspended } from "./step" 6 6 7 7 /** 8 8 * Continuation - what to do with the result of an effect. ··· 21 21 export const interpret = <A, E, R>( 22 22 eff: Eff<A, E, R>, 23 23 cont: Continuation<A, E, R>, 24 - makeFiber: <A2, E2>(eff: Eff<A2, E2, R>, env: R) => Fiber<A2, E2> 24 + makeFiber: <A2, E2>(eff: Eff<A2, E2, R>, env: R) => Fiber<A2, E2>, 25 25 ): Step<unknown, unknown> => 26 26 match(eff)({ 27 27 Succeed: ({ value }) => cont.onSuccess(value), 28 28 29 29 Fail: ({ error }) => cont.onFailure(error), 30 30 31 - Sync: ({ f }) => suspended(() => { 32 - try { 33 - return cont.onSuccess(f()) 34 - } catch (e) { 35 - return cont.onFailure(e as E) 36 - } 37 - }), 31 + Sync: ({ f }) => 32 + suspended(() => { 33 + try { 34 + return cont.onSuccess(f()) 35 + } catch (e) { 36 + return cont.onFailure(e as E) 37 + } 38 + }), 38 39 39 40 Async: ({ register }) => { 40 41 let result: Exit<A, E> | null = null 41 42 let callback: (() => void) | null = null 42 43 43 - const cleanup = register(exit => { 44 + const cleanup = register((exit) => { 44 45 result = exit 45 46 callback?.() 46 47 }) 47 48 48 49 const handleExit = matchExit<A, E, Step<unknown, unknown>>( 49 - value => cont.onSuccess(value), 50 - error => cont.onFailure(error), 51 - () => cont.onFailure("interrupted" as E) 50 + (value) => cont.onSuccess(value), 51 + (error) => cont.onFailure(error), 52 + () => cont.onFailure("interrupted" as E), 52 53 ) 53 54 54 55 // Handle synchronous completion ··· 58 59 59 60 return blocked( 60 61 cleanup, 61 - cb => { callback = cb }, 62 - () => handleExit(result!) 62 + (cb) => { 63 + callback = cb 64 + }, 65 + () => handleExit(result!), 63 66 ) 64 67 }, 65 68 66 - FlatMap: ({ effect, f }) => suspended(() => 67 - interpret(effect as Eff<unknown, E, R>, { 68 - env: cont.env, 69 - onSuccess: a => interpret(f(a) as Eff<A, E, R>, cont, makeFiber), 70 - onFailure: cont.onFailure 71 - }, makeFiber) 72 - ), 69 + FlatMap: ({ effect, f }) => 70 + suspended(() => 71 + interpret( 72 + effect as Eff<unknown, E, R>, 73 + { 74 + env: cont.env, 75 + onSuccess: (a) => interpret(f(a) as Eff<A, E, R>, cont, makeFiber), 76 + onFailure: cont.onFailure, 77 + }, 78 + makeFiber, 79 + ), 80 + ), 73 81 74 - Fold: ({ effect, onErr, onSucc }) => suspended(() => 75 - interpret(effect as Eff<unknown, unknown, R>, { 76 - env: cont.env, 77 - onSuccess: a => interpret(onSucc(a) as Eff<A, E, R>, cont, makeFiber), 78 - onFailure: e => interpret(onErr(e) as Eff<A, E, R>, cont, makeFiber) 79 - }, makeFiber) 80 - ), 82 + Fold: ({ effect, onErr, onSucc }) => 83 + suspended(() => 84 + interpret( 85 + effect as Eff<unknown, unknown, R>, 86 + { 87 + env: cont.env, 88 + onSuccess: (a) => 89 + interpret(onSucc(a) as Eff<A, E, R>, cont, makeFiber), 90 + onFailure: (e) => 91 + interpret(onErr(e) as Eff<A, E, R>, cont, makeFiber), 92 + }, 93 + makeFiber, 94 + ), 95 + ), 81 96 82 97 Access: ({ f }) => cont.onSuccess(f(cont.env)), 83 98 84 - AccessEff: ({ f }) => suspended(() => 85 - interpret(f(cont.env), cont, makeFiber) 86 - ), 99 + AccessEff: ({ f }) => 100 + suspended(() => interpret(f(cont.env), cont, makeFiber)), 87 101 88 - Provide: ({ effect, env }) => suspended(() => 89 - interpret(effect as Eff<A, E, R>, { ...cont, env: env as R }, makeFiber) 90 - ), 102 + Provide: ({ effect, env }) => 103 + suspended(() => 104 + interpret( 105 + effect as Eff<A, E, R>, 106 + { ...cont, env: env as R }, 107 + makeFiber, 108 + ), 109 + ), 91 110 92 111 Fork: ({ effect }) => { 93 112 const fiber = makeFiber(effect, cont.env) ··· 96 115 97 116 YieldNow: () => suspended(() => cont.onSuccess(undefined as A)), 98 117 99 - GetFiberId: () => cont.onSuccess((cont.fiberId ?? makeFiberId()) as unknown as A) 118 + GetFiberId: () => 119 + cont.onSuccess((cont.fiberId ?? makeFiberId()) as unknown as A), 100 120 })
+34 -19
src/effect/step.ts
··· 1 - import type { Exit } from "./exit" 2 1 import { match } from "../prelude/match" 2 + import type { Exit } from "./exit" 3 3 4 4 /** 5 5 * Step<A, E> - Result of pure interpretation. ··· 14 14 export type Step<A, E> = 15 15 | { readonly _tag: "Done"; readonly exit: Exit<A, E> } 16 16 | { readonly _tag: "Suspended"; readonly resume: () => Step<A, E> } 17 - | { readonly _tag: "Blocked"; readonly cleanup: () => void; readonly onComplete: (cb: () => void) => void; readonly next: () => Step<A, E> } 17 + | { 18 + readonly _tag: "Blocked" 19 + readonly cleanup: () => void 20 + readonly onComplete: (cb: () => void) => void 21 + readonly next: () => Step<A, E> 22 + } 18 23 19 24 // Constructors 20 - export const done = <A, E>(exit: Exit<A, E>): Step<A, E> => 21 - ({ _tag: "Done", exit }) 25 + export const done = <A, E>(exit: Exit<A, E>): Step<A, E> => ({ 26 + _tag: "Done", 27 + exit, 28 + }) 22 29 23 - export const suspended = <A, E>(resume: () => Step<A, E>): Step<A, E> => 24 - ({ _tag: "Suspended", resume }) 30 + export const suspended = <A, E>(resume: () => Step<A, E>): Step<A, E> => ({ 31 + _tag: "Suspended", 32 + resume, 33 + }) 25 34 26 35 export const blocked = <A, E>( 27 36 cleanup: () => void, 28 37 onComplete: (cb: () => void) => void, 29 - next: () => Step<A, E> 30 - ): Step<A, E> => 31 - ({ _tag: "Blocked", cleanup, onComplete, next }) 38 + next: () => Step<A, E>, 39 + ): Step<A, E> => ({ _tag: "Blocked", cleanup, onComplete, next }) 32 40 33 41 // Match helper 34 - export const matchStep = <A, E, R>( 35 - onDone: (exit: Exit<A, E>) => R, 36 - onSuspended: (resume: () => Step<A, E>) => R, 37 - onBlocked: (cleanup: () => void, onComplete: (cb: () => void) => void, next: () => Step<A, E>) => R 38 - ) => (step: Step<A, E>): R => 39 - match(step)({ 40 - Done: ({ exit }) => onDone(exit), 41 - Suspended: ({ resume }) => onSuspended(resume), 42 - Blocked: ({ cleanup, onComplete, next }) => onBlocked(cleanup, onComplete, next) 43 - }) 42 + export const matchStep = 43 + <A, E, R>( 44 + onDone: (exit: Exit<A, E>) => R, 45 + onSuspended: (resume: () => Step<A, E>) => R, 46 + onBlocked: ( 47 + cleanup: () => void, 48 + onComplete: (cb: () => void) => void, 49 + next: () => Step<A, E>, 50 + ) => R, 51 + ) => 52 + (step: Step<A, E>): R => 53 + match(step)({ 54 + Done: ({ exit }) => onDone(exit), 55 + Suspended: ({ resume }) => onSuspended(resume), 56 + Blocked: ({ cleanup, onComplete, next }) => 57 + onBlocked(cleanup, onComplete, next), 58 + })
+47 -31
src/effect/trampoline.ts
··· 1 1 import { match } from "../prelude/match" 2 2 import type { Option } from "../prelude/option" 3 - import { some, none, matchOption } from "../prelude/option" 3 + import { matchOption, none, some } from "../prelude/option" 4 4 import type { Eff, Fiber, FiberId } from "./eff" 5 - import { makeFiberId, async as asyncEff } from "./eff" 5 + import { async as asyncEff, makeFiberId } from "./eff" 6 6 import type { Exit } from "./exit" 7 - import { success, failure, interrupted } from "./exit" 8 - import { done, type Step } from "./step" 7 + import { failure, interrupted, success } from "./exit" 9 8 import { interpret } from "./interpret" 9 + import { done, type Step } from "./step" 10 10 11 11 /** 12 12 * ESCAPE HATCH: The boundary between pure effect interpretation and JS execution. ··· 41 41 Promise.resolve().then(() => trampoline(resume())), 42 42 43 43 Blocked: ({ onComplete, next }) => 44 - new Promise(resolve => { 44 + new Promise((resolve) => { 45 45 onComplete(() => resolve(trampoline(next()))) 46 - }) 46 + }), 47 47 }) 48 48 49 49 // Fiber counter for unique IDs ··· 61 61 62 62 const makeFiberFn = <A2, E2>(e: Eff<A2, E2, R>, r: R) => createFiber(e, r) 63 63 64 - const initialStep = interpret(eff, { 65 - env, 66 - fiberId: fId, 67 - onSuccess: (a: A) => done(success(a)), 68 - onFailure: (e: E) => done(failure(e)) 69 - }, makeFiberFn) as Step<A, E> 64 + const initialStep = interpret( 65 + eff, 66 + { 67 + env, 68 + fiberId: fId, 69 + onSuccess: (a: A) => done(success(a)), 70 + onFailure: (e: E) => done(failure(e)), 71 + }, 72 + makeFiberFn, 73 + ) as Step<A, E> 70 74 71 75 // Start execution 72 - trampoline(initialStep).then(exit => { 73 - const finalExit: Exit<A, E> = isInterrupted ? interrupted(id) as Exit<A, E> : exit 76 + trampoline(initialStep).then((exit) => { 77 + const finalExit: Exit<A, E> = isInterrupted 78 + ? (interrupted(id) as Exit<A, E>) 79 + : exit 74 80 exitResult = some(finalExit) 75 - awaiters.forEach(cb => cb(finalExit)) 81 + awaiters.forEach((cb) => cb(finalExit)) 76 82 }) 77 83 78 84 const fiber: Fiber<A, E> = { 79 85 _tag: "Fiber", 80 86 id, 81 - await: () => matchOption( 82 - (exit: Exit<A, E>) => Promise.resolve(exit), 83 - () => new Promise<Exit<A, E>>(resolve => awaiters.push(resolve)) 84 - )(exitResult), 85 - interrupt: () => { isInterrupted = true }, 86 - join: (): Eff<A, E, unknown> => asyncEff(resume => { 87 - fiber.await().then(resume) 88 - return () => {} 89 - }) 87 + await: () => 88 + matchOption( 89 + (exit: Exit<A, E>) => Promise.resolve(exit), 90 + () => new Promise<Exit<A, E>>((resolve) => awaiters.push(resolve)), 91 + )(exitResult), 92 + interrupt: () => { 93 + isInterrupted = true 94 + }, 95 + join: (): Eff<A, E, unknown> => 96 + asyncEff((resume) => { 97 + fiber.await().then(resume) 98 + return () => {} 99 + }), 90 100 } 91 101 return fiber 92 102 } ··· 100 110 /** 101 111 * Run an effect and return a Promise of the Exit. 102 112 */ 103 - export const runPromiseExit = <A, E>(eff: Eff<A, E, unknown>): Promise<Exit<A, E>> => 104 - runFiber(eff, {}).await() 113 + export const runPromiseExit = <A, E>( 114 + eff: Eff<A, E, unknown>, 115 + ): Promise<Exit<A, E>> => runFiber(eff, {}).await() 105 116 106 117 /** 107 118 * Run an effect and return a Promise of the success value. 108 119 * Throws if the effect fails. 109 120 */ 110 121 export const runPromise = <A>(eff: Eff<A, unknown, unknown>): Promise<A> => 111 - runPromiseExit(eff).then(exit => 122 + runPromiseExit(eff).then((exit) => 112 123 match(exit)({ 113 124 Success: ({ value }) => value, 114 - Failure: ({ error }) => { throw error }, 115 - Interrupted: ({ by }) => { throw new Error(`Interrupted by ${by}`) } 116 - }) 125 + Failure: ({ error }) => { 126 + throw error 127 + }, 128 + Interrupted: ({ by }) => { 129 + throw new Error(`Interrupted by ${by}`) 130 + }, 131 + }), 117 132 ) 118 133 119 134 /** 120 135 * Run an effect with provided environment. 121 136 */ 122 - export const runPromiseWith = <A, E, R>(env: R) => 137 + export const runPromiseWith = 138 + <A, E, R>(env: R) => 123 139 (eff: Eff<A, E, R>): Promise<Exit<A, E>> => 124 140 runFiber(eff, env).await()
+2 -2
src/index.ts
··· 8 8 * - Fiber-based concurrency (cancellation, racing, parallelism) 9 9 */ 10 10 11 + export * from "./data" 12 + export * from "./effect" 11 13 // Re-export all public API from modular structure 12 14 export * from "./prelude" 13 - export * from "./data" 14 - export * from "./effect"
+7 -6
src/prelude/brand.ts
··· 1 1 import type { Option } from "./option" 2 - import { some, none } from "./option" 2 + import { none, some } from "./option" 3 3 4 4 // Brand symbol for nominal typing 5 5 declare const __brand: unique symbol ··· 29 29 * Create a refinement validator. 30 30 * Returns Option to avoid throwing on invalid input. 31 31 */ 32 - export const refine = <T, R>(predicate: (value: T) => boolean) => 32 + export const refine = 33 + <T, R>(predicate: (value: T) => boolean) => 33 34 (value: T): Option<Refined<T, R>> => 34 35 predicate(value) ? some(value as Refined<T, R>) : none 35 36 ··· 39 40 export type Normalized = "Normalized" 40 41 export type Integer = "Integer" 41 42 42 - export const positive = refine<number, Positive>(x => x > 0) 43 - export const nonNegative = refine<number, NonNegative>(x => x >= 0) 44 - export const normalized = refine<number, Normalized>(x => x >= 0 && x <= 1) 45 - export const integer = refine<number, Integer>(x => Number.isInteger(x)) 43 + export const positive = refine<number, Positive>((x) => x > 0) 44 + export const nonNegative = refine<number, NonNegative>((x) => x >= 0) 45 + export const normalized = refine<number, Normalized>((x) => x >= 0 && x <= 1) 46 + export const integer = refine<number, Integer>((x) => Number.isInteger(x))
+16 -10
src/prelude/compose.ts
··· 28 28 * always42() // 42 29 29 * ``` 30 30 */ 31 - export const constant = <A>(a: A) => (): A => a 31 + export const constant = 32 + <A>(a: A) => 33 + (): A => 34 + a 32 35 33 36 /** 34 37 * Flip the arguments of a curried binary function. ··· 41 44 * flipped(10)(3) // -7 42 45 * ``` 43 46 */ 44 - export const flip = <A, B, C>(f: (a: A) => (b: B) => C) => 47 + export const flip = 48 + <A, B, C>(f: (a: A) => (b: B) => C) => 45 49 (b: B) => 46 50 (a: A): C => 47 51 f(a)(b) ··· 61 65 * // Returns: 84 62 66 * ``` 63 67 */ 64 - export const tap = <A>(f: (a: A) => void) => 68 + export const tap = 69 + <A>(f: (a: A) => void) => 65 70 (a: A): A => (f(a), a) 66 71 67 72 /** ··· 77 82 * ) 78 83 * ``` 79 84 */ 80 - export const trace = <T>(label: string) => tap<T>(x => console.log(label, x)) 85 + export const trace = <T>(label: string) => tap<T>((x) => console.log(label, x)) 81 86 82 87 /** 83 88 * Conditional helper - choose between two functions based on a predicate. ··· 91 96 * // Returns: "was true" 92 97 * ``` 93 98 */ 94 - export const ifElse = <T, E>(predicate: boolean) => 99 + export const ifElse = 100 + <T, E>(predicate: boolean) => 95 101 (onFalse: () => T | E, onTrue: () => T | E): T | E => 96 102 predicate ? onFalse() : onTrue() 97 103 ··· 134 140 a: A, 135 141 ab: (a: A) => B, 136 142 bc: (b: B) => C, 137 - cd: (c: C) => D 143 + cd: (c: C) => D, 138 144 ): D 139 145 export function pipe<A, B, C, D, E>( 140 146 a: A, 141 147 ab: (a: A) => B, 142 148 bc: (b: B) => C, 143 149 cd: (c: C) => D, 144 - de: (d: D) => E 150 + de: (d: D) => E, 145 151 ): E 146 152 export function pipe<A, B, C, D, E, F>( 147 153 a: A, ··· 149 155 bc: (b: B) => C, 150 156 cd: (c: C) => D, 151 157 de: (d: D) => E, 152 - ef: (e: E) => F 158 + ef: (e: E) => F, 153 159 ): F 154 160 export function pipe( 155 161 a: unknown, ··· 189 195 export function flow<A, B, C, D>( 190 196 ab: (a: A) => B, 191 197 bc: (b: B) => C, 192 - cd: (c: C) => D 198 + cd: (c: C) => D, 193 199 ): (a: A) => D 194 200 export function flow<A, B, C, D, E>( 195 201 ab: (a: A) => B, 196 202 bc: (b: B) => C, 197 203 cd: (c: C) => D, 198 - de: (d: D) => E 204 + de: (d: D) => E, 199 205 ): (a: A) => E 200 206 export function flow( 201 207 ...fns: Array<(x: unknown) => unknown>
+3 -3
src/prelude/index.ts
··· 1 + export * from "./brand" 1 2 export * from "./compose" 2 - export * from "./result" 3 - export * from "./option" 4 3 export * from "./match" 5 - export * from "./brand" 4 + export * from "./option" 5 + export * from "./result" 6 6 export * from "./typeclasses"
+28 -18
src/prelude/match.ts
··· 41 41 * }) 42 42 * ``` 43 43 */ 44 - export const match = <T extends { _tag: string }>(value: T) => 44 + export const match = 45 + <T extends { _tag: string }>(value: T) => 45 46 <R>(cases: { [K in T["_tag"]]: (v: Extract<T, { _tag: K }>) => R }): R => 46 47 (cases as unknown as Record<string, (v: T) => R>)[value._tag]!(value) 47 48 ··· 66 67 * }) 67 68 * ``` 68 69 */ 69 - export const matchOr = <T extends { _tag: string }, R>(defaultValue: R) => 70 + export const matchOr = 71 + <T extends { _tag: string }, R>(defaultValue: R) => 70 72 (value: T) => 71 - (cases: Partial<{ [K in T["_tag"]]: (v: Extract<T, { _tag: K }>) => R }>): R => 72 - (cases as unknown as Record<string, ((v: T) => R) | undefined>)[value._tag] !== undefined 73 - ? (cases as unknown as Record<string, (v: T) => R>)[value._tag]!(value) 74 - : defaultValue 73 + ( 74 + cases: Partial<{ [K in T["_tag"]]: (v: Extract<T, { _tag: K }>) => R }>, 75 + ): R => 76 + (cases as unknown as Record<string, ((v: T) => R) | undefined>)[ 77 + value._tag 78 + ] !== undefined 79 + ? (cases as unknown as Record<string, (v: T) => R>)[value._tag]!(value) 80 + : defaultValue 75 81 76 82 /** 77 83 * Guard-based pattern matching with predicates. ··· 105 111 * )(() => "F") 106 112 * ``` 107 113 */ 108 - export const when = <T>(value: T) => 114 + export const when = 115 + <T>(value: T) => 109 116 <R>(...guards: ReadonlyArray<readonly [(v: T) => boolean, (v: T) => R]>) => 110 - (defaultCase: (v: T) => R): R => { 111 - const go = (index: number): R => 112 - index >= guards.length 113 - ? defaultCase(value) 114 - : guards[index]![0](value) 115 - ? guards[index]![1](value) 116 - : go(index + 1) 117 - return go(0) 118 - } 117 + (defaultCase: (v: T) => R): R => { 118 + const go = (index: number): R => 119 + index >= guards.length 120 + ? defaultCase(value) 121 + : guards[index]![0](value) 122 + ? guards[index]![1](value) 123 + : go(index + 1) 124 + return go(0) 125 + } 119 126 120 127 /** 121 128 * Match on literal values (strings, numbers, booleans). ··· 145 152 * }, "weekday") 146 153 * ``` 147 154 */ 148 - export const matchLiteral = <T extends string | number | boolean>(value: T) => 155 + export const matchLiteral = 156 + <T extends string | number | boolean>(value: T) => 149 157 <R>(cases: { [K in T & PropertyKey]: R }, defaultCase?: R): R => 150 - (value as PropertyKey) in cases ? cases[value as T & PropertyKey] : (defaultCase as R) 158 + (value as PropertyKey) in cases 159 + ? cases[value as T & PropertyKey] 160 + : (defaultCase as R)
+10 -8
src/prelude/option.ts
··· 108 108 * pipe(none, mapOption(n => n * 2)) // None 109 109 * ``` 110 110 */ 111 - export const mapOption = <T, U>(f: (t: T) => U) => 111 + export const mapOption = 112 + <T, U>(f: (t: T) => U) => 112 113 (option: Option<T>): Option<U> => 113 114 option._tag === "Some" ? some(f(option.value)) : none 114 115 ··· 127 128 * ) // Some(email) or None 128 129 * ``` 129 130 */ 130 - export const flatMapOption = <T, U>(f: (t: T) => Option<U>) => 131 + export const flatMapOption = 132 + <T, U>(f: (t: T) => Option<U>) => 131 133 (option: Option<T>): Option<U> => 132 134 option._tag === "Some" ? f(option.value) : none 133 135 ··· 140 142 * pipe(none, getOrElse(0)) // 0 141 143 * ``` 142 144 */ 143 - export const getOrElse = <T>(defaultValue: T) => 145 + export const getOrElse = 146 + <T>(defaultValue: T) => 144 147 (option: Option<T>): T => 145 148 option._tag === "Some" ? option.value : defaultValue 146 149 ··· 155 158 * )(option) 156 159 * ``` 157 160 */ 158 - export const matchOption = <T, R>( 159 - onSome: (value: T) => R, 160 - onNone: () => R 161 - ) => (option: Option<T>): R => 162 - option._tag === "Some" ? onSome(option.value) : onNone() 161 + export const matchOption = 162 + <T, R>(onSome: (value: T) => R, onNone: () => R) => 163 + (option: Option<T>): R => 164 + option._tag === "Some" ? onSome(option.value) : onNone() 163 165 164 166 /** 165 167 * Convert an Option to a nullable value.
+16 -14
src/prelude/result.ts
··· 103 103 * ) // Err("oops") - unchanged 104 104 * ``` 105 105 */ 106 - export const mapResult = <T, U, E>(f: (t: T) => U) => 106 + export const mapResult = 107 + <T, U, E>(f: (t: T) => U) => 107 108 (result: Result<T, E>): Result<U, E> => 108 109 result._tag === "Ok" ? ok(f(result.value)) : result 109 110 ··· 119 120 * ) 120 121 * ``` 121 122 */ 122 - export const mapErr = <T, E, F>(f: (e: E) => F) => 123 + export const mapErr = 124 + <T, E, F>(f: (e: E) => F) => 123 125 (result: Result<T, E>): Result<T, F> => 124 126 result._tag === "Err" ? err(f(result.error)) : result 125 127 ··· 146 148 * ) // Err("OOPS") 147 149 * ``` 148 150 */ 149 - export const bimap = <T, U, E, F>( 150 - onOk: (t: T) => U, 151 - onErr: (e: E) => F 152 - ) => (result: Result<T, E>): Result<U, F> => 153 - result._tag === "Ok" ? ok(onOk(result.value)) : err(onErr(result.error)) 151 + export const bimap = 152 + <T, U, E, F>(onOk: (t: T) => U, onErr: (e: E) => F) => 153 + (result: Result<T, E>): Result<U, F> => 154 + result._tag === "Ok" ? ok(onOk(result.value)) : err(onErr(result.error)) 154 155 155 156 /** 156 157 * Chain Result-returning operations. ··· 172 173 * ) 173 174 * ``` 174 175 */ 175 - export const chainResult = <T, U, E>(f: (t: T) => Result<U, E>) => 176 + export const chainResult = 177 + <T, U, E>(f: (t: T) => Result<U, E>) => 176 178 (result: Result<T, E>): Result<U, E> => 177 179 result._tag === "Ok" ? f(result.value) : result 178 180 ··· 185 187 * pipe(err("oops"), unwrapOr(0)) // 0 186 188 * ``` 187 189 */ 188 - export const unwrapOr = <T, E>(defaultValue: T) => 190 + export const unwrapOr = 191 + <T, E>(defaultValue: T) => 189 192 (result: Result<T, E>): T => 190 193 result._tag === "Ok" ? result.value : defaultValue 191 194 ··· 200 203 * )(result) 201 204 * ``` 202 205 */ 203 - export const matchResult = <T, E, R>( 204 - onOk: (value: T) => R, 205 - onErr: (error: E) => R 206 - ) => (result: Result<T, E>): R => 207 - result._tag === "Ok" ? onOk(result.value) : onErr(result.error) 206 + export const matchResult = 207 + <T, E, R>(onOk: (value: T) => R, onErr: (error: E) => R) => 208 + (result: Result<T, E>): R => 209 + result._tag === "Ok" ? onOk(result.value) : onErr(result.error) 208 210 209 211 /** 210 212 * ESCAPE HATCH: Bridges thrown exceptions to Result values.
+18 -16
src/prelude/typeclasses.ts
··· 27 27 * @module prelude/typeclasses 28 28 */ 29 29 30 - import { 31 - ok, mapResult, chainResult, bimap, 32 - type Result 33 - } from "./result" 34 - import { 35 - some, mapOption, flatMapOption, 36 - type Option 37 - } from "./option" 30 + import { flatMapOption, mapOption, type Option, some } from "./option" 31 + import { bimap, chainResult, mapResult, ok, type Result } from "./result" 38 32 39 33 // ----------------------------------------------------------------------------- 40 34 // Type Class Interfaces (Educational) ··· 151 145 * ) // Err("no function") 152 146 * ``` 153 147 */ 154 - export const apResult = <A, E>(ra: Result<A, E>) => 148 + export const apResult = 149 + <A, E>(ra: Result<A, E>) => 155 150 <B>(rf: Result<(a: A) => B, E>): Result<B, E> => 156 151 rf._tag === "Ok" && ra._tag === "Ok" 157 152 ? ok(rf.value(ra.value)) 158 - : rf._tag === "Err" ? rf : ra as Result<B, E> 153 + : rf._tag === "Err" 154 + ? rf 155 + : (ra as Result<B, E>) 159 156 160 157 // ----------------------------------------------------------------------------- 161 158 // Applicative Functions for Option ··· 177 174 * ) // None 178 175 * ``` 179 176 */ 180 - export const apOption = <A>(oa: Option<A>) => 177 + export const apOption = 178 + <A>(oa: Option<A>) => 181 179 <B>(of_: Option<(a: A) => B>): Option<B> => 182 180 of_._tag === "Some" && oa._tag === "Some" 183 181 ? some(of_.value(oa.value)) ··· 206 204 flatMap: chainResult, 207 205 208 206 // Bifunctor 209 - bimap 207 + bimap, 210 208 } as const 211 209 212 210 /** ··· 221 219 ap: apOption, 222 220 223 221 // Monad 224 - flatMap: flatMapOption 222 + flatMap: flatMapOption, 225 223 } as const 226 224 227 225 // ----------------------------------------------------------------------------- ··· 240 238 * addResults(ok(1), err("!")) // Err("!") 241 239 * ``` 242 240 */ 243 - export const liftA2Result = <A, B, C, E>(f: (a: A, b: B) => C) => 241 + export const liftA2Result = 242 + <A, B, C, E>(f: (a: A, b: B) => C) => 244 243 (ra: Result<A, E>, rb: Result<B, E>): Result<C, E> => 245 244 ra._tag === "Ok" && rb._tag === "Ok" 246 245 ? ok(f(ra.value, rb.value)) 247 - : ra._tag === "Err" ? ra : rb as Result<C, E> 246 + : ra._tag === "Err" 247 + ? ra 248 + : (rb as Result<C, E>) 248 249 249 250 /** 250 251 * Lift a binary function to work with Options. ··· 258 259 * addOptions(some(1), none) // None 259 260 * ``` 260 261 */ 261 - export const liftA2Option = <A, B, C>(f: (a: A, b: B) => C) => 262 + export const liftA2Option = 263 + <A, B, C>(f: (a: A, b: B) => C) => 262 264 (oa: Option<A>, ob: Option<B>): Option<C> => 263 265 oa._tag === "Some" && ob._tag === "Some" 264 266 ? some(f(oa.value, ob.value))
+33 -7
tests/array.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { arr, sortNum, sortBy, nonEmpty, map, filter, reduce, head, last, binarySearchNum, take, drop, pipe } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + arr, 4 + binarySearchNum, 5 + drop, 6 + filter, 7 + head, 8 + last, 9 + map, 10 + nonEmpty, 11 + pipe, 12 + reduce, 13 + sortBy, 14 + sortNum, 15 + take, 16 + } from "../src/index" 3 17 4 18 describe("Tracked Arrays", () => { 5 19 describe("arr", () => { ··· 16 30 }) 17 31 18 32 it("sortBy sorts by key function", () => { 19 - const users = arr([{ name: "Bob", age: 30 }, { name: "Alice", age: 25 }]) 20 - const result = sortBy<typeof users[number], never>(u => u.age)(users) 33 + const users = arr([ 34 + { name: "Bob", age: 30 }, 35 + { name: "Alice", age: 25 }, 36 + ]) 37 + const result = sortBy<(typeof users)[number], never>((u) => u.age)(users) 21 38 expect(result[0]!.name).toBe("Alice") 22 39 }) 23 40 }) ··· 52 69 53 70 describe("map, filter, reduce", () => { 54 71 it("map transforms elements", () => { 55 - const result = pipe(arr([1, 2, 3]), map(x => x * 2)) 72 + const result = pipe( 73 + arr([1, 2, 3]), 74 + map((x) => x * 2), 75 + ) 56 76 expect([...result]).toEqual([2, 4, 6]) 57 77 }) 58 78 59 79 it("filter keeps matching elements", () => { 60 - const result = pipe(arr([1, 2, 3, 4]), filter(x => x % 2 === 0)) 80 + const result = pipe( 81 + arr([1, 2, 3, 4]), 82 + filter((x) => x % 2 === 0), 83 + ) 61 84 expect([...result]).toEqual([2, 4]) 62 85 }) 63 86 64 87 it("reduce folds array", () => { 65 - const result = pipe(arr([1, 2, 3, 4]), reduce((acc, x) => acc + x, 0)) 88 + const result = pipe( 89 + arr([1, 2, 3, 4]), 90 + reduce((acc, x) => acc + x, 0), 91 + ) 66 92 expect(result).toBe(10) 67 93 }) 68 94 })
+4 -3
tests/brand.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { brand, type Branded } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { type Branded, brand } from "../src/index" 3 3 4 4 describe("Branded Types", () => { 5 5 it("creates branded values", () => { 6 6 type UserId = Branded<string, "UserId"> 7 7 const id: UserId = brand("user-123") 8 - expect(id).toBe("user-123") 8 + // Branded types are the underlying primitive at runtime 9 + expect(String(id)).toBe("user-123") 9 10 }) 10 11 11 12 it("preserves underlying value operations", () => {
+16 -5
tests/composition.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { pipe, flow, id, constant, flip, tap } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { constant, flip, flow, id, pipe, tap } from "../src/index" 3 3 4 4 describe("Composition", () => { 5 5 describe("pipe", () => { ··· 8 8 }) 9 9 10 10 it("pipes through functions", () => { 11 - const result = pipe(5, x => x + 1, x => x * 2, x => `result: ${x}`) 11 + const result = pipe( 12 + 5, 13 + (x) => x + 1, 14 + (x) => x * 2, 15 + (x) => `result: ${x}`, 16 + ) 12 17 expect(result).toBe("result: 12") 13 18 }) 14 19 }) 15 20 16 21 describe("flow", () => { 17 22 it("composes functions left-to-right", () => { 18 - const process = flow((x: number) => x + 1, x => x * 2, x => `result: ${x}`) 23 + const process = flow( 24 + (x: number) => x + 1, 25 + (x) => x * 2, 26 + (x) => `result: ${x}`, 27 + ) 19 28 expect(process(5)).toBe("result: 12") 20 29 }) 21 30 }) ··· 39 48 40 49 it("tap performs side effect and returns value", () => { 41 50 let sideEffect = 0 42 - const result = tap<number>(x => { sideEffect = x })(42) 51 + const result = tap<number>((x) => { 52 + sideEffect = x 53 + })(42) 43 54 expect(result).toBe(42) 44 55 expect(sideEffect).toBe(42) 45 56 })
+40 -11
tests/effect-basic.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 1 + import { describe, expect, it } from "bun:test" 2 2 import { 3 - succeed, fail, sync, attempt, fromPromise, 4 - mapEff, flatMap, foldEff, catchAll, access, provide, 5 - pipe, runPromise, runPromiseExit, Exit 3 + access, 4 + attempt, 5 + catchAll, 6 + Exit, 7 + fail, 8 + flatMap, 9 + foldEff, 10 + fromPromise, 11 + mapEff, 12 + pipe, 13 + provide, 14 + runPromise, 15 + runPromiseExit, 16 + succeed, 17 + sync, 6 18 } from "../src/index" 7 19 8 20 describe("Effect - Basic Operations", () => { ··· 26 38 }) 27 39 28 40 it("attempt catches thrown errors", async () => { 29 - const effect = attempt(() => { throw new Error("boom") }) 41 + const effect = attempt(() => { 42 + throw new Error("boom") 43 + }) 30 44 const exit = await runPromiseExit(effect) 31 45 expect(Exit.isFailure(exit)).toBe(true) 32 46 }) ··· 39 53 40 54 describe("transformations", () => { 41 55 it("mapEff transforms success value", async () => { 42 - const result = await runPromise(pipe(succeed(5), mapEff(x => x * 2))) 56 + const result = await runPromise( 57 + pipe( 58 + succeed(5), 59 + mapEff((x) => x * 2), 60 + ), 61 + ) 43 62 expect(result).toBe(10) 44 63 }) 45 64 46 65 it("flatMap chains effects", async () => { 47 66 const result = await runPromise( 48 - pipe(succeed(5), flatMap(x => succeed(x * 2)), flatMap(x => succeed(x + 1))) 67 + pipe( 68 + succeed(5), 69 + flatMap((x) => succeed(x * 2)), 70 + flatMap((x) => succeed(x + 1)), 71 + ), 49 72 ) 50 73 expect(result).toBe(11) 51 74 }) ··· 53 76 it("foldEff handles both success and failure", async () => { 54 77 const handle = foldEff( 55 78 (e: string) => succeed(`error: ${e}`), 56 - (a: number) => succeed(`success: ${a}`) 79 + (a: number) => succeed(`success: ${a}`), 57 80 ) 58 81 59 82 expect(await runPromise(pipe(succeed(42), handle))).toBe("success: 42") ··· 62 85 63 86 it("catchAll recovers from errors", async () => { 64 87 const result = await runPromise( 65 - pipe(fail("error"), catchAll(() => succeed("recovered"))) 88 + pipe( 89 + fail("error"), 90 + catchAll(() => succeed("recovered")), 91 + ), 66 92 ) 67 93 expect(result).toBe("recovered") 68 94 }) ··· 72 98 type Config = { baseUrl: string } 73 99 74 100 it("access reads from environment", async () => { 75 - const getBaseUrl = access<Config, string>(env => env.baseUrl) 101 + const getBaseUrl = access<Config, string>((env) => env.baseUrl) 76 102 const result = await runPromise( 77 - pipe(getBaseUrl, provide<Config>({ baseUrl: "https://api.example.com" })) 103 + pipe( 104 + getBaseUrl, 105 + provide<Config>({ baseUrl: "https://api.example.com" }), 106 + ), 78 107 ) 79 108 expect(result).toBe("https://api.example.com") 80 109 })
+120 -34
tests/effect-concurrency.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 1 + import { describe, expect, it } from "bun:test" 2 2 import { 3 - succeed, fail, sleep, fork, join, race, all, timeout, retry, 4 - traverse, sequence, traversePar, sequencePar, 5 - mapEff, flatMap, pipe, runPromise, runPromiseExit, runFiber, Exit 3 + all, 4 + Exit, 5 + fail, 6 + flatMap, 7 + fork, 8 + join, 9 + mapEff, 10 + pipe, 11 + race, 12 + retry, 13 + runFiber, 14 + runPromise, 15 + runPromiseExit, 16 + sequence, 17 + sequencePar, 18 + sleep, 19 + succeed, 20 + timeout, 21 + traverse, 22 + traversePar, 6 23 } from "../src/index" 7 24 8 25 describe("Effect - Concurrency", () => { ··· 13 30 }) 14 31 15 32 it("join awaits fiber result", async () => { 16 - const program = pipe(fork(succeed(42)), flatMap(fiber => join(fiber))) 33 + const program = pipe( 34 + fork(succeed(42)), 35 + flatMap((fiber) => join(fiber)), 36 + ) 17 37 const result = await runPromise(program) 18 38 expect(result).toBe(42) 19 39 }) ··· 21 41 22 42 describe("race", () => { 23 43 it("returns first to complete", async () => { 24 - const slow = pipe(sleep(100), mapEff(() => "slow")) 25 - const fast = pipe(sleep(10), mapEff(() => "fast")) 44 + const slow = pipe( 45 + sleep(100), 46 + mapEff(() => "slow"), 47 + ) 48 + const fast = pipe( 49 + sleep(10), 50 + mapEff(() => "fast"), 51 + ) 26 52 const result = await runPromise(race(slow, fast)) 27 53 expect(result).toBe("fast") 28 54 }) ··· 31 57 describe("all", () => { 32 58 it("runs effects in parallel", async () => { 33 59 const start = Date.now() 34 - const results = await runPromise(all([ 35 - pipe(sleep(50), mapEff(() => "a")), 36 - pipe(sleep(50), mapEff(() => "b")), 37 - pipe(sleep(50), mapEff(() => "c")), 38 - ])) 60 + const results = await runPromise( 61 + all([ 62 + pipe( 63 + sleep(50), 64 + mapEff(() => "a"), 65 + ), 66 + pipe( 67 + sleep(50), 68 + mapEff(() => "b"), 69 + ), 70 + pipe( 71 + sleep(50), 72 + mapEff(() => "c"), 73 + ), 74 + ]), 75 + ) 39 76 const elapsed = Date.now() - start 40 77 41 78 expect(results).toEqual(["a", "b", "c"]) ··· 43 80 }) 44 81 45 82 it("fails fast on first error", async () => { 46 - const exit = await runPromiseExit(all([ 47 - pipe(sleep(10), flatMap(() => fail("first error"))), 48 - pipe(sleep(100), mapEff(() => "never")), 49 - ])) 83 + const exit = await runPromiseExit( 84 + all([ 85 + pipe( 86 + sleep(10), 87 + flatMap(() => fail("first error")), 88 + ), 89 + pipe( 90 + sleep(100), 91 + mapEff(() => "never"), 92 + ), 93 + ]), 94 + ) 50 95 expect(Exit.isFailure(exit)).toBe(true) 51 96 }) 52 97 }) ··· 58 103 }) 59 104 60 105 it("returns null if times out", async () => { 61 - const slow = pipe(sleep(200), mapEff(() => "done")) 106 + const slow = pipe( 107 + sleep(200), 108 + mapEff(() => "done"), 109 + ) 62 110 const result = await runPromise(timeout(50)(slow)) 63 111 expect(result).toBeNull() 64 112 }) ··· 72 120 flatMap(() => { 73 121 attempts++ 74 122 return attempts < 3 ? fail("not yet") : succeed("success") 75 - }) 123 + }), 76 124 ) 77 125 const result = await runPromise(retry(5)(flaky)) 78 126 expect(result).toBe("success") ··· 82 130 83 131 describe("interruption", () => { 84 132 it("fiber can be interrupted", async () => { 85 - const fiber = runFiber(pipe(sleep(1000), mapEff(() => "done")), {}) 133 + const fiber = runFiber( 134 + pipe( 135 + sleep(1000), 136 + mapEff(() => "done"), 137 + ), 138 + {}, 139 + ) 86 140 setTimeout(() => fiber.interrupt(), 20) 87 141 const exit = await fiber.await() 88 142 expect(Exit.isInterrupted(exit)).toBe(true) ··· 92 146 describe("traverse", () => { 93 147 it("applies effect-producing function to each element sequentially", async () => { 94 148 const order: number[] = [] 95 - const f = (n: number) => pipe( 96 - sleep(10), 97 - mapEff(() => { 98 - order.push(n) 99 - return n * 2 100 - }) 101 - ) 149 + const f = (n: number) => 150 + pipe( 151 + sleep(10), 152 + mapEff(() => { 153 + order.push(n) 154 + return n * 2 155 + }), 156 + ) 102 157 103 158 const result = await runPromise(pipe([1, 2, 3], traverse(f))) 104 159 expect(result).toEqual([2, 4, 6]) ··· 140 195 it("runs sequentially", async () => { 141 196 const order: string[] = [] 142 197 const effects = [ 143 - pipe(sleep(10), mapEff(() => { order.push("a"); return "a" })), 144 - pipe(sleep(10), mapEff(() => { order.push("b"); return "b" })), 198 + pipe( 199 + sleep(10), 200 + mapEff(() => { 201 + order.push("a") 202 + return "a" 203 + }), 204 + ), 205 + pipe( 206 + sleep(10), 207 + mapEff(() => { 208 + order.push("b") 209 + return "b" 210 + }), 211 + ), 145 212 ] 146 213 await runPromise(sequence(effects)) 147 214 expect(order).toEqual(["a", "b"]) ··· 151 218 describe("traversePar", () => { 152 219 it("applies function in parallel", async () => { 153 220 const start = Date.now() 154 - const f = (n: number) => pipe(sleep(50), mapEff(() => n * 2)) 221 + const f = (n: number) => 222 + pipe( 223 + sleep(50), 224 + mapEff(() => n * 2), 225 + ) 155 226 156 227 const result = await runPromise(pipe([1, 2, 3], traversePar(f))) 157 228 const elapsed = Date.now() - start ··· 163 234 it("fails fast on first error", async () => { 164 235 const f = (n: number) => 165 236 n === 2 166 - ? pipe(sleep(10), flatMap(() => fail("error"))) 167 - : pipe(sleep(100), mapEff(() => n)) 237 + ? pipe( 238 + sleep(10), 239 + flatMap(() => fail("error")), 240 + ) 241 + : pipe( 242 + sleep(100), 243 + mapEff(() => n), 244 + ) 168 245 169 246 const exit = await runPromiseExit(pipe([1, 2, 3], traversePar(f))) 170 247 expect(Exit.isFailure(exit)).toBe(true) ··· 181 258 it("runs in parallel", async () => { 182 259 const start = Date.now() 183 260 const effects = [ 184 - pipe(sleep(50), mapEff(() => "a")), 185 - pipe(sleep(50), mapEff(() => "b")), 186 - pipe(sleep(50), mapEff(() => "c")), 261 + pipe( 262 + sleep(50), 263 + mapEff(() => "a"), 264 + ), 265 + pipe( 266 + sleep(50), 267 + mapEff(() => "b"), 268 + ), 269 + pipe( 270 + sleep(50), 271 + mapEff(() => "c"), 272 + ), 187 273 ] 188 274 const result = await runPromise(sequencePar(effects)) 189 275 const elapsed = Date.now() - start
+29 -19
tests/exit.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { Exit, success, failure, interrupted, bimapExit, pipe } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + bimapExit, 4 + Exit, 5 + failure, 6 + interrupted, 7 + pipe, 8 + success, 9 + } from "../src/index" 3 10 4 11 describe("Exit", () => { 5 12 describe("constructors", () => { 6 13 it("success creates Success variant", () => { 7 14 const exit = success(42) 8 - expect(exit._tag).toBe("Success") 9 - expect(exit.value).toBe(42) 15 + expect(Exit.isSuccess(exit)).toBe(true) 16 + if (Exit.isSuccess(exit)) expect(exit.value).toBe(42) 10 17 }) 11 18 12 19 it("failure creates Failure variant", () => { 13 20 const exit = failure("error") 14 - expect(exit._tag).toBe("Failure") 15 - expect(exit.error).toBe("error") 21 + expect(Exit.isFailure(exit)).toBe(true) 22 + if (Exit.isFailure(exit)) expect(exit.error).toBe("error") 16 23 }) 17 24 18 25 it("interrupted creates Interrupted variant", () => { 19 26 const exit = interrupted("user") 20 - expect(exit._tag).toBe("Interrupted") 21 - expect(exit.by).toBe("user") 27 + expect(Exit.isInterrupted(exit)).toBe(true) 28 + if (Exit.isInterrupted(exit)) expect(exit.by).toBe("user") 22 29 }) 23 30 }) 24 31 ··· 47 54 const exit = pipe( 48 55 success(10), 49 56 bimapExit( 50 - n => n * 2, 51 - (e: string) => e.toUpperCase() 52 - ) 57 + (n) => n * 2, 58 + (e: string) => e.toUpperCase(), 59 + ), 53 60 ) 54 61 expect(exit).toEqual(success(20)) 55 62 }) ··· 59 66 failure("oops"), 60 67 bimapExit( 61 68 (n: number) => n * 2, 62 - e => e.toUpperCase() 63 - ) 69 + (e) => e.toUpperCase(), 70 + ), 64 71 ) 65 72 expect(exit).toEqual(failure("OOPS")) 66 73 }) ··· 70 77 interrupted("user-cancel"), 71 78 bimapExit( 72 79 (n: number) => n * 2, 73 - (e: string) => e.toUpperCase() 74 - ) 80 + (e: string) => e.toUpperCase(), 81 + ), 75 82 ) 76 83 expect(exit).toEqual(interrupted("user-cancel")) 77 84 }) ··· 80 87 const exit = pipe( 81 88 success(42), 82 89 bimapExit( 83 - n => n.toString(), 84 - (e: string) => ({ code: 500, message: e }) 85 - ) 90 + (n) => n.toString(), 91 + (e: string) => ({ code: 500, message: e }), 92 + ), 86 93 ) 87 94 expect(exit).toEqual(success("42")) 88 95 }) ··· 92 99 it("is available via Exit namespace", () => { 93 100 const exit = pipe( 94 101 success(5), 95 - Exit.bimap(n => n + 1, (e: string) => e) 102 + Exit.bimap( 103 + (n) => n + 1, 104 + (e: string) => e, 105 + ), 96 106 ) 97 107 expect(exit).toEqual(success(6)) 98 108 })
+9 -2
tests/guards.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { isString, isNumber, isBoolean, and, or, isPositive, isInteger } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + and, 4 + isBoolean, 5 + isNumber, 6 + isPositive, 7 + isString, 8 + or, 9 + } from "../src/index" 3 10 4 11 describe("Guards", () => { 5 12 describe("primitive guards", () => {
+19 -5
tests/option.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { some, none, fromNullable, mapOption, getOrElse, flatMapOption, pipe } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + flatMapOption, 4 + fromNullable, 5 + getOrElse, 6 + mapOption, 7 + none, 8 + pipe, 9 + some, 10 + } from "../src/index" 3 11 4 12 describe("Option", () => { 5 13 describe("constructors", () => { ··· 30 38 31 39 describe("mapOption", () => { 32 40 it("transforms Some values", () => { 33 - const result = pipe(some(5), mapOption(x => x * 2)) 41 + const result = pipe( 42 + some(5), 43 + mapOption((x) => x * 2), 44 + ) 34 45 expect(result).toEqual(some(10)) 35 46 }) 36 47 37 48 it("passes through None", () => { 38 - const result = pipe(none, mapOption((x: number) => x * 2)) 49 + const result = pipe( 50 + none, 51 + mapOption((x: number) => x * 2), 52 + ) 39 53 expect(result).toEqual(none) 40 54 }) 41 55 }) 42 56 43 57 describe("flatMapOption", () => { 44 58 it("chains Some values", () => { 45 - const safeSqrt = (x: number) => x >= 0 ? some(Math.sqrt(x)) : none 59 + const safeSqrt = (x: number) => (x >= 0 ? some(Math.sqrt(x)) : none) 46 60 const result = pipe(some(16), flatMapOption(safeSqrt)) 47 61 expect(result).toEqual(some(4)) 48 62 })
+22 -11
tests/pattern-matching.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { match, matchOr, when, matchResult, matchOption, ok, err, some, none } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + err, 4 + match, 5 + matchOption, 6 + matchOr, 7 + matchResult, 8 + none, 9 + ok, 10 + some, 11 + when, 12 + } from "../src/index" 3 13 4 14 describe("Pattern Matching", () => { 5 15 type Shape = ··· 22 32 describe("matchOr", () => { 23 33 it("uses default for unhandled variants", () => { 24 34 const describe = matchOr<Shape, string>("unknown") 25 - const result = describe({ _tag: "Rectangle", width: 1, height: 1 })({ Circle: () => "a circle" }) 35 + const result = describe({ _tag: "Rectangle", width: 1, height: 1 })({ 36 + Circle: () => "a circle", 37 + }) 26 38 expect(result).toBe("unknown") 27 39 }) 28 40 }) ··· 30 42 describe("when", () => { 31 43 it("matches predicates in order", () => { 32 44 const grade = (score: number) => 33 - when(score)( 34 - [s => s >= 90, () => "A"], 35 - [s => s >= 80, () => "B"], 36 - )(() => "F") 45 + when(score)([(s) => s >= 90, () => "A"], [(s) => s >= 80, () => "B"])( 46 + () => "F", 47 + ) 37 48 38 49 expect(grade(95)).toBe("A") 39 50 expect(grade(85)).toBe("B") ··· 44 55 describe("matchResult", () => { 45 56 it("matches Ok and Err", () => { 46 57 const format = matchResult<number, string, string>( 47 - v => `value: ${v}`, 48 - e => `error: ${e}` 58 + (v) => `value: ${v}`, 59 + (e) => `error: ${e}`, 49 60 ) 50 61 expect(format(ok(42))).toBe("value: 42") 51 62 expect(format(err("fail"))).toBe("error: fail") ··· 55 66 describe("matchOption", () => { 56 67 it("matches Some and None", () => { 57 68 const format = matchOption<number, string>( 58 - v => `found: ${v}`, 59 - () => "empty" 69 + (v) => `found: ${v}`, 70 + () => "empty", 60 71 ) 61 72 expect(format(some(42))).toBe("found: 42") 62 73 expect(format(none)).toBe("empty")
+55 -20
tests/refinement.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { refine, positive, nonNegative, normalized, integer, some, none } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + integer, 4 + isNone, 5 + isSome, 6 + nonNegative, 7 + normalized, 8 + positive, 9 + refine, 10 + } from "../src/index" 3 11 4 12 describe("Refinements", () => { 5 13 describe("positive", () => { 6 14 it("accepts positive numbers", () => { 7 - expect(positive(5)).toEqual(some(5)) 8 - expect(positive(0.001)).toEqual(some(0.001)) 15 + const r1 = positive(5) 16 + expect(isSome(r1)).toBe(true) 17 + if (isSome(r1)) expect(Number(r1.value)).toBe(5) 18 + 19 + const r2 = positive(0.001) 20 + expect(isSome(r2)).toBe(true) 21 + if (isSome(r2)) expect(Number(r2.value)).toBe(0.001) 9 22 }) 10 23 11 24 it("rejects non-positive numbers", () => { 12 - expect(positive(0)).toEqual(none) 13 - expect(positive(-1)).toEqual(none) 25 + expect(isNone(positive(0))).toBe(true) 26 + expect(isNone(positive(-1))).toBe(true) 14 27 }) 15 28 }) 16 29 17 30 describe("nonNegative", () => { 18 31 it("accepts zero and positive", () => { 19 - expect(nonNegative(0)).toEqual(some(0)) 20 - expect(nonNegative(100)).toEqual(some(100)) 32 + const r1 = nonNegative(0) 33 + expect(isSome(r1)).toBe(true) 34 + if (isSome(r1)) expect(Number(r1.value)).toBe(0) 35 + 36 + const r2 = nonNegative(100) 37 + expect(isSome(r2)).toBe(true) 38 + if (isSome(r2)) expect(Number(r2.value)).toBe(100) 21 39 }) 22 40 23 41 it("rejects negative numbers", () => { 24 - expect(nonNegative(-0.001)).toEqual(none) 42 + expect(isNone(nonNegative(-0.001))).toBe(true) 25 43 }) 26 44 }) 27 45 28 46 describe("normalized", () => { 29 47 it("accepts values in [0, 1]", () => { 30 - expect(normalized(0)).toEqual(some(0)) 31 - expect(normalized(0.5)).toEqual(some(0.5)) 32 - expect(normalized(1)).toEqual(some(1)) 48 + const r1 = normalized(0) 49 + expect(isSome(r1)).toBe(true) 50 + if (isSome(r1)) expect(Number(r1.value)).toBe(0) 51 + 52 + const r2 = normalized(0.5) 53 + expect(isSome(r2)).toBe(true) 54 + if (isSome(r2)) expect(Number(r2.value)).toBe(0.5) 55 + 56 + const r3 = normalized(1) 57 + expect(isSome(r3)).toBe(true) 58 + if (isSome(r3)) expect(Number(r3.value)).toBe(1) 33 59 }) 34 60 35 61 it("rejects values outside [0, 1]", () => { 36 - expect(normalized(-0.1)).toEqual(none) 37 - expect(normalized(1.1)).toEqual(none) 62 + expect(isNone(normalized(-0.1))).toBe(true) 63 + expect(isNone(normalized(1.1))).toBe(true) 38 64 }) 39 65 }) 40 66 41 67 describe("integer", () => { 42 68 it("accepts integers", () => { 43 - expect(integer(42)).toEqual(some(42)) 44 - expect(integer(-10)).toEqual(some(-10)) 69 + const r1 = integer(42) 70 + expect(isSome(r1)).toBe(true) 71 + if (isSome(r1)) expect(Number(r1.value)).toBe(42) 72 + 73 + const r2 = integer(-10) 74 + expect(isSome(r2)).toBe(true) 75 + if (isSome(r2)) expect(Number(r2.value)).toBe(-10) 45 76 }) 46 77 47 78 it("rejects non-integers", () => { 48 - expect(integer(3.14)).toEqual(none) 79 + expect(isNone(integer(3.14))).toBe(true) 49 80 }) 50 81 }) 51 82 52 83 describe("custom refinements", () => { 53 84 it("creates custom validators", () => { 54 - const even = refine<number, "Even">(x => x % 2 === 0) 55 - expect(even(4)).toEqual(some(4)) 56 - expect(even(3)).toEqual(none) 85 + const even = refine<number, "Even">((x) => x % 2 === 0) 86 + 87 + const r1 = even(4) 88 + expect(isSome(r1)).toBe(true) 89 + if (isSome(r1)) expect(Number(r1.value)).toBe(4) 90 + 91 + expect(isNone(even(3))).toBe(true) 57 92 }) 58 93 }) 59 94 })
+35 -14
tests/result.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { ok, err, mapResult, mapErr, chainResult, unwrapOr, tryCatch, bimap, pipe } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + bimap, 4 + chainResult, 5 + err, 6 + mapResult, 7 + ok, 8 + pipe, 9 + tryCatch, 10 + unwrapOr, 11 + } from "../src/index" 3 12 4 13 describe("Result", () => { 5 14 describe("constructors", () => { ··· 18 27 19 28 describe("mapResult", () => { 20 29 it("transforms Ok values", () => { 21 - const result = pipe(ok(5), mapResult(x => x * 2)) 30 + const result = pipe( 31 + ok(5), 32 + mapResult((x) => x * 2), 33 + ) 22 34 expect(result).toEqual(ok(10)) 23 35 }) 24 36 25 37 it("passes through Err unchanged", () => { 26 - const result = pipe(err("error"), mapResult((x: number) => x * 2)) 38 + const result = pipe( 39 + err("error"), 40 + mapResult((x: number) => x * 2), 41 + ) 27 42 expect(result).toEqual(err("error")) 28 43 }) 29 44 }) ··· 33 48 const divide = (a: number, b: number) => 34 49 b === 0 ? err("div by zero") : ok(a / b) 35 50 36 - const result = pipe(ok(10), chainResult(x => divide(x, 2))) 51 + const result = pipe( 52 + ok(10), 53 + chainResult((x) => divide(x, 2)), 54 + ) 37 55 expect(result).toEqual(ok(5)) 38 56 }) 39 57 40 58 it("short-circuits on Err", () => { 41 - const result = pipe(err("first error"), chainResult(() => ok(42))) 59 + const result = pipe( 60 + err("first error"), 61 + chainResult(() => ok(42)), 62 + ) 42 63 expect(result).toEqual(err("first error")) 43 64 }) 44 65 }) ··· 70 91 const result = pipe( 71 92 ok(10), 72 93 bimap( 73 - n => n * 2, 74 - (e: string) => e.toUpperCase() 75 - ) 94 + (n) => n * 2, 95 + (e: string) => e.toUpperCase(), 96 + ), 76 97 ) 77 98 expect(result).toEqual(ok(20)) 78 99 }) ··· 82 103 err("oops"), 83 104 bimap( 84 105 (n: number) => n * 2, 85 - e => e.toUpperCase() 86 - ) 106 + (e) => e.toUpperCase(), 107 + ), 87 108 ) 88 109 expect(result).toEqual(err("OOPS")) 89 110 }) ··· 92 113 const result = pipe( 93 114 ok(42), 94 115 bimap( 95 - n => n.toString(), 96 - (e: string) => ({ message: e }) 97 - ) 116 + (n) => n.toString(), 117 + (e: string) => ({ message: e }), 118 + ), 98 119 ) 99 120 expect(result).toEqual(ok("42")) 100 121 })
+47 -22
tests/typeclasses.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 1 + import { describe, expect, it } from "bun:test" 2 2 import { 3 - ok, err, some, none, pipe, 4 - apResult, apOption, liftA2Result, liftA2Option, 5 - resultInstances, optionInstances 3 + apOption, 4 + apResult, 5 + err, 6 + liftA2Option, 7 + liftA2Result, 8 + none, 9 + ok, 10 + optionInstances, 11 + pipe, 12 + resultInstances, 13 + some, 6 14 } from "../src/index" 7 15 8 16 describe("Type Classes", () => { ··· 37 45 38 46 it("works with curried functions", () => { 39 47 const add = (a: number) => (b: number) => a + b 40 - const result = pipe( 41 - ok(add), 42 - apResult(ok(10)), 43 - apResult(ok(5)) 44 - ) 48 + const result = pipe(ok(add), apResult(ok(10)), apResult(ok(5))) 45 49 expect(result).toEqual(ok(15)) 46 50 }) 47 51 }) ··· 70 74 71 75 it("works with curried functions", () => { 72 76 const add = (a: number) => (b: number) => a + b 73 - const result = pipe( 74 - some(add), 75 - apOption(some(10)), 76 - apOption(some(5)) 77 - ) 77 + const result = pipe(some(add), apOption(some(10)), apOption(some(5))) 78 78 expect(result).toEqual(some(15)) 79 79 }) 80 80 }) ··· 134 134 135 135 describe("resultInstances", () => { 136 136 it("has map function", () => { 137 - const result = pipe(ok(5), resultInstances.map(x => x * 2)) 137 + const result = pipe( 138 + ok(5), 139 + resultInstances.map((x) => x * 2), 140 + ) 138 141 expect(result).toEqual(ok(10)) 139 142 }) 140 143 ··· 143 146 }) 144 147 145 148 it("has ap function", () => { 146 - const result = pipe(ok((x: number) => x + 1), resultInstances.ap(ok(5))) 149 + const result = pipe( 150 + ok((x: number) => x + 1), 151 + resultInstances.ap(ok(5)), 152 + ) 147 153 expect(result).toEqual(ok(6)) 148 154 }) 149 155 150 156 it("has flatMap function", () => { 151 - const result = pipe(ok(5), resultInstances.flatMap(x => ok(x * 2))) 157 + const result = pipe( 158 + ok(5), 159 + resultInstances.flatMap((x) => ok(x * 2)), 160 + ) 152 161 expect(result).toEqual(ok(10)) 153 162 }) 154 163 155 164 it("has bimap function", () => { 156 165 const result = pipe( 157 166 ok(5), 158 - resultInstances.bimap(x => x * 2, (e: string) => e.toUpperCase()) 167 + resultInstances.bimap( 168 + (x) => x * 2, 169 + (e: string) => e.toUpperCase(), 170 + ), 159 171 ) 160 172 expect(result).toEqual(ok(10)) 161 173 }) ··· 163 175 164 176 describe("optionInstances", () => { 165 177 it("has map function", () => { 166 - const result = pipe(some(5), optionInstances.map(x => x * 2)) 178 + const result = pipe( 179 + some(5), 180 + optionInstances.map((x) => x * 2), 181 + ) 167 182 expect(result).toEqual(some(10)) 168 183 }) 169 184 ··· 172 187 }) 173 188 174 189 it("has ap function", () => { 175 - const result = pipe(some((x: number) => x + 1), optionInstances.ap(some(5))) 190 + const result = pipe( 191 + some((x: number) => x + 1), 192 + optionInstances.ap(some(5)), 193 + ) 176 194 expect(result).toEqual(some(6)) 177 195 }) 178 196 179 197 it("has flatMap function", () => { 180 - const result = pipe(some(5), optionInstances.flatMap(x => some(x * 2))) 198 + const result = pipe( 199 + some(5), 200 + optionInstances.flatMap((x) => some(x * 2)), 201 + ) 181 202 expect(result).toEqual(some(10)) 182 203 }) 183 204 }) ··· 196 217 197 218 const original = ok(5) 198 219 const left = pipe(original, resultInstances.map(fg)) 199 - const right = pipe(original, resultInstances.map(g), resultInstances.map(f)) 220 + const right = pipe( 221 + original, 222 + resultInstances.map(g), 223 + resultInstances.map(f), 224 + ) 200 225 201 226 expect(left).toEqual(right) 202 227 })
+8 -5
tests/typestate.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { entity, transition, pipe, type Entity } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { type Entity, entity, pipe, transition } from "../src/index" 3 3 4 4 describe("Typestate", () => { 5 5 type Doc = { title: string; content: string } ··· 10 10 entity<Doc, "draft">({ title, content: "" }) 11 11 12 12 const edit = (content: string) => 13 - transition<Doc, "draft", "draft">(d => ({ ...d, content })) 13 + transition<Doc, "draft", "draft">((d) => ({ ...d, content })) 14 14 15 - const publish: (d: Draft) => Published = 16 - transition<Doc, "draft", "published">() 15 + const publish: (d: Draft) => Published = transition< 16 + Doc, 17 + "draft", 18 + "published" 19 + >() 17 20 18 21 it("creates entities with initial state", () => { 19 22 const doc = createDraft("My Post")
+11 -2
tests/units.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 2 - import { meters, seconds, kilograms, velocity, addQ, subQ, scaleQ, unquantity } from "../src/index" 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + addQ, 4 + kilograms, 5 + meters, 6 + scaleQ, 7 + seconds, 8 + subQ, 9 + unquantity, 10 + velocity, 11 + } from "../src/index" 3 12 4 13 describe("Units", () => { 5 14 describe("quantity constructors", () => {
+45 -24
tests/validation.test.ts
··· 1 - import { describe, it, expect } from "bun:test" 1 + import { describe, expect, it } from "bun:test" 2 2 import { 3 - valid, invalid, invalidOne, isValid, isInvalid, 4 - mapValidation, apValidation, 5 - fromResult, toResult, toResultAll, 6 - matchValidation, getErrors, getValue, 7 - ok, err, pipe 3 + apValidation, 4 + err, 5 + fromResult, 6 + getErrors, 7 + getValue, 8 + invalid, 9 + invalidOne, 10 + isInvalid, 11 + isValid, 12 + mapValidation, 13 + matchValidation, 14 + ok, 15 + pipe, 16 + toResult, 17 + toResultAll, 18 + valid, 8 19 } from "../src/index" 9 20 10 21 describe("Validation", () => { ··· 42 53 43 54 describe("mapValidation", () => { 44 55 it("transforms Valid value", () => { 45 - const result = pipe(valid(10), mapValidation(n => n * 2)) 56 + const result = pipe( 57 + valid(10), 58 + mapValidation((n) => n * 2), 59 + ) 46 60 expect(result).toEqual(valid(20)) 47 61 }) 48 62 49 63 it("passes Invalid through unchanged", () => { 50 - const result = pipe(invalid(["err"]), mapValidation((n: number) => n * 2)) 64 + const result = pipe( 65 + invalid(["err"]), 66 + mapValidation((n: number) => n * 2), 67 + ) 51 68 expect(result).toEqual(invalid(["err"])) 52 69 }) 53 70 }) ··· 83 100 84 101 it("accumulates multiple errors from multiple validations", () => { 85 102 type Person = { name: string; age: number; email: string } 86 - const makePerson = (name: string) => (age: number) => (email: string): Person => 87 - ({ name, age, email }) 103 + const makePerson = 104 + (name: string) => 105 + (age: number) => 106 + (email: string): Person => ({ name, age, email }) 88 107 89 108 const validateName = (name: string) => 90 109 name.length > 0 ? valid(name) : invalid(["Name required"]) ··· 100 119 valid(makePerson), 101 120 apValidation(validateName("")), 102 121 apValidation(validateAge(-5)), 103 - apValidation(validateEmail("bad")) 122 + apValidation(validateEmail("bad")), 104 123 ) 105 124 106 - expect(result).toEqual(invalid([ 107 - "Name required", 108 - "Age must be non-negative", 109 - "Invalid email" 110 - ])) 125 + expect(result).toEqual( 126 + invalid(["Name required", "Age must be non-negative", "Invalid email"]), 127 + ) 111 128 }) 112 129 113 130 it("returns valid when all validations pass", () => { 114 131 type Person = { name: string; age: number } 115 - const makePerson = (name: string) => (age: number): Person => ({ name, age }) 132 + const makePerson = 133 + (name: string) => 134 + (age: number): Person => ({ name, age }) 116 135 117 136 const validateName = (name: string) => 118 137 name.length > 0 ? valid(name) : invalid(["Name required"]) ··· 123 142 const result = pipe( 124 143 valid(makePerson), 125 144 apValidation(validateName("Alice")), 126 - apValidation(validateAge(30)) 145 + apValidation(validateAge(30)), 127 146 ) 128 147 129 148 expect(result).toEqual(valid({ name: "Alice", age: 30 })) ··· 156 175 }) 157 176 158 177 it("converts Invalid to Err with all errors", () => { 159 - expect(toResultAll(invalid(["err1", "err2"]))).toEqual(err(["err1", "err2"])) 178 + expect(toResultAll(invalid(["err1", "err2"]))).toEqual( 179 + err(["err1", "err2"]), 180 + ) 160 181 }) 161 182 }) 162 183 ··· 165 186 const result = pipe( 166 187 valid(42), 167 188 matchValidation( 168 - v => `value: ${v}`, 169 - e => `errors: ${e.join(", ")}` 170 - ) 189 + (v) => `value: ${v}`, 190 + (e) => `errors: ${e.join(", ")}`, 191 + ), 171 192 ) 172 193 expect(result).toBe("value: 42") 173 194 }) ··· 177 198 invalid(["e1", "e2"]), 178 199 matchValidation( 179 200 (v: number) => `value: ${v}`, 180 - e => `errors: ${e.join(", ")}` 181 - ) 201 + (e) => `errors: ${e.join(", ")}`, 202 + ), 182 203 ) 183 204 expect(result).toBe("errors: e1, e2") 184 205 })
+8
tsconfig.test.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "compilerOptions": { 4 + "rootDir": ".", 5 + "noEmit": true 6 + }, 7 + "include": ["src/**/*.ts", "tests/**/*.ts"] 8 + }