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 optics module with Lens, Prism, Optional, Traversal, type-safe compose, and law-based tests

+1180 -1
+2 -1
README.md
··· 10 10 - **Refinements** — Encode validated properties in the type system (`Positive`, `NonEmpty`, `Sorted`) 11 11 - **Result & Option** — Handle errors and nullability as values, not exceptions 12 12 - **Pattern Matching** — Exhaustive, type-safe matching on discriminated unions 13 + - **Optics** — Composable, law-abiding immutable data access (Lens, Prism, Optional, Traversal) 13 14 - **Effect System** — Composable effects with typed errors and dependency injection 14 15 - **Fiber Runtime** — Cooperative concurrency with cancellation, racing, and parallelism 15 - - **Zero Dependencies** — Single-file library, ~950 lines of TypeScript 16 + - **Zero Dependencies** — Pure TypeScript library with zero runtime dependencies 16 17 17 18 ## Installation 18 19
+1
src/index.ts
··· 10 10 11 11 export * from "./data" 12 12 export * from "./effect" 13 + export * from "./optics" 13 14 // Re-export all public API from modular structure 14 15 export * from "./prelude"
+68
src/optics/bridges.ts
··· 1 + /** 2 + * Pre-built optics for purus-ts data types. 3 + * 4 + * Bridges connect the optics module to Result, Option, and Validation, 5 + * providing ready-to-use Prisms for each variant and a Traversal for arrays. 6 + * 7 + * @module optics/bridges 8 + */ 9 + 10 + import type { Result } from "../prelude/result" 11 + import type { Option } from "../prelude/option" 12 + import type { Validation } from "../data/validation" 13 + import type { Prism, Traversal } from "./types" 14 + import { ok, err } from "../prelude/result" 15 + import { some, none } from "../prelude/option" 16 + import { valid } from "../data/validation" 17 + import { prism } from "./prism" 18 + import { traversal } from "./traversal" 19 + 20 + /** 21 + * Prism focusing on the Ok variant of a Result. 22 + * preview extracts the success value; review wraps a value in Ok. 23 + */ 24 + export const okPrism = <T, E>(): Prism<Result<T, E>, T> => 25 + prism( 26 + (r) => (r._tag === "Ok" ? some(r.value) : none), 27 + (t) => ok(t), 28 + ) 29 + 30 + /** 31 + * Prism focusing on the Err variant of a Result. 32 + * preview extracts the error value; review wraps a value in Err. 33 + */ 34 + export const errPrism = <T, E>(): Prism<Result<T, E>, E> => 35 + prism( 36 + (r) => (r._tag === "Err" ? some(r.error) : none), 37 + (e) => err(e), 38 + ) 39 + 40 + /** 41 + * Prism focusing on the Some variant of an Option. 42 + * preview extracts the wrapped value; review wraps a value in Some. 43 + */ 44 + export const somePrism = <T>(): Prism<Option<T>, T> => 45 + prism( 46 + (o) => (o._tag === "Some" ? some(o.value) : none), 47 + (t) => some(t), 48 + ) 49 + 50 + /** 51 + * Prism focusing on the Valid variant of a Validation. 52 + * preview extracts the success value; review wraps a value in Valid. 53 + */ 54 + export const validPrism = <A, E>(): Prism<Validation<A, E>, A> => 55 + prism( 56 + (v) => (v._tag === "Valid" ? some(v.value) : none), 57 + (a) => valid(a), 58 + ) 59 + 60 + /** 61 + * Traversal focusing on every element of a readonly array. 62 + * getAll returns all elements; modify applies a function to each element. 63 + */ 64 + export const eachTraversal = <A>(): Traversal<readonly A[], A> => 65 + traversal( 66 + (s) => s, 67 + (f) => (s) => s.map(f), 68 + )
+137
src/optics/compose.ts
··· 1 + /** 2 + * Type-safe optic composition. 3 + * 4 + * Composition table (outer \ inner → result): 5 + * 6 + * | outer \ inner | Lens | Prism | Optional | Traversal | 7 + * |---------------|-----------|----------|----------|-----------| 8 + * | Lens | Lens | Optional | Optional | Traversal | 9 + * | Prism | Optional | Prism | Optional | Traversal | 10 + * | Optional | Optional | Optional | Optional | Traversal | 11 + * | Traversal | Traversal | Traversal| Traversal| Traversal | 12 + * 13 + * @module optics/compose 14 + */ 15 + 16 + import { isSome, flatMapOption, mapOption } from "../prelude/option" 17 + import type { Lens, Optional, Prism, Traversal } from "./types" 18 + 19 + // ── Most specific overloads ────────────────────────────────────────────────── 20 + 21 + export function compose<S, A, B>(outer: Lens<S, A>, inner: Lens<A, B>): Lens<S, B> 22 + export function compose<S, A, B>(outer: Prism<S, A>, inner: Prism<A, B>): Prism<S, B> 23 + 24 + // ── Optional-yielding overloads ────────────────────────────────────────────── 25 + 26 + export function compose<S, A, B>(outer: Lens<S, A>, inner: Prism<A, B>): Optional<S, B> 27 + export function compose<S, A, B>(outer: Prism<S, A>, inner: Lens<A, B>): Optional<S, B> 28 + export function compose<S, A, B>(outer: Lens<S, A>, inner: Optional<A, B>): Optional<S, B> 29 + export function compose<S, A, B>(outer: Optional<S, A>, inner: Lens<A, B>): Optional<S, B> 30 + export function compose<S, A, B>(outer: Prism<S, A>, inner: Optional<A, B>): Optional<S, B> 31 + export function compose<S, A, B>(outer: Optional<S, A>, inner: Prism<A, B>): Optional<S, B> 32 + export function compose<S, A, B>(outer: Optional<S, A>, inner: Optional<A, B>): Optional<S, B> 33 + 34 + // ── Traversal-yielding overloads (any + Traversal, Traversal + any) ────────── 35 + 36 + export function compose<S, A, B>(outer: Lens<S, A>, inner: Traversal<A, B>): Traversal<S, B> 37 + export function compose<S, A, B>(outer: Prism<S, A>, inner: Traversal<A, B>): Traversal<S, B> 38 + export function compose<S, A, B>(outer: Optional<S, A>, inner: Traversal<A, B>): Traversal<S, B> 39 + export function compose<S, A, B>(outer: Traversal<S, A>, inner: Lens<A, B>): Traversal<S, B> 40 + export function compose<S, A, B>(outer: Traversal<S, A>, inner: Prism<A, B>): Traversal<S, B> 41 + export function compose<S, A, B>(outer: Traversal<S, A>, inner: Optional<A, B>): Traversal<S, B> 42 + export function compose<S, A, B>(outer: Traversal<S, A>, inner: Traversal<A, B>): Traversal<S, B> 43 + 44 + // ── Implementation ─────────────────────────────────────────────────────────── 45 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 + export function compose(outer: any, inner: any): any { 47 + const ot = outer._tag as string 48 + const it = inner._tag as string 49 + 50 + if (ot === "Lens" && it === "Lens") { 51 + return { 52 + _tag: "Lens", 53 + get: (s: unknown) => inner.get(outer.get(s)), 54 + set: (b: unknown) => (s: unknown) => outer.set(inner.set(b)(outer.get(s)))(s), 55 + } 56 + } 57 + 58 + if (ot === "Prism" && it === "Prism") { 59 + return { 60 + _tag: "Prism", 61 + preview: (s: unknown) => flatMapOption(inner.preview)(outer.preview(s)), 62 + review: (b: unknown) => outer.review(inner.review(b)), 63 + } 64 + } 65 + 66 + // Lens + Prism → Optional 67 + if (ot === "Lens" && it === "Prism") { 68 + return { 69 + _tag: "Optional", 70 + getOption: (s: unknown) => inner.preview(outer.get(s)), 71 + set: (b: unknown) => (s: unknown) => outer.set(inner.review(b))(s), 72 + } 73 + } 74 + 75 + // Prism + Lens → Optional 76 + if (ot === "Prism" && it === "Lens") { 77 + return { 78 + _tag: "Optional", 79 + getOption: (s: unknown) => mapOption(inner.get)(outer.preview(s)), 80 + set: (b: unknown) => (s: unknown) => { 81 + const oa = outer.preview(s) 82 + return isSome(oa) ? outer.review(inner.set(b)(oa.value)) : s 83 + }, 84 + } 85 + } 86 + 87 + // Optional-yielding: covers Lens+Opt, Opt+Lens, Prism+Opt, Opt+Prism, Opt+Opt 88 + if (ot !== "Traversal" && it !== "Traversal") { 89 + const outerGet = (s: unknown) => 90 + ot === "Lens" ? { _tag: "Some" as const, value: outer.get(s) } 91 + : (ot === "Prism" ? outer.preview(s) : outer.getOption(s)) 92 + return { 93 + _tag: "Optional", 94 + getOption: (s: unknown) => { 95 + const oa = outerGet(s) 96 + if (!isSome(oa)) return oa 97 + return it === "Lens" ? { _tag: "Some" as const, value: inner.get(oa.value) } 98 + : it === "Prism" ? inner.preview(oa.value) 99 + : inner.getOption(oa.value) 100 + }, 101 + set: (b: unknown) => (s: unknown) => { 102 + const oa = outerGet(s) 103 + if (!isSome(oa)) return s 104 + const newA = it === "Prism" ? inner.review(b) : inner.set(b)(oa.value) 105 + return ot === "Lens" ? outer.set(newA)(s) 106 + : ot === "Prism" ? outer.review(newA) : outer.set(newA)(s) 107 + }, 108 + } 109 + } 110 + 111 + // Traversal-yielding: at least one side is a Traversal 112 + const outerGetAll = (s: unknown): readonly unknown[] => 113 + ot === "Lens" ? [outer.get(s)] 114 + : ot === "Prism" ? (isSome(outer.preview(s)) ? [outer.preview(s).value] : []) 115 + : ot === "Optional" ? (isSome(outer.getOption(s)) ? [outer.getOption(s).value] : []) 116 + : outer.getAll(s) 117 + const innerGetAll = (a: unknown): readonly unknown[] => 118 + it === "Lens" ? [inner.get(a)] 119 + : it === "Prism" ? (isSome(inner.preview(a)) ? [inner.preview(a).value] : []) 120 + : it === "Optional" ? (isSome(inner.getOption(a)) ? [inner.getOption(a).value] : []) 121 + : inner.getAll(a) 122 + const innerModify = (f: (x: unknown) => unknown) => (a: unknown): unknown => 123 + it === "Lens" ? inner.set(f(inner.get(a)))(a) 124 + : it === "Prism" ? (isSome(inner.preview(a)) ? inner.review(f(inner.preview(a).value)) : a) 125 + : it === "Optional" ? (isSome(inner.getOption(a)) ? inner.set(f(inner.getOption(a).value))(a) : a) 126 + : inner.modify(f)(a) 127 + const outerModify = (g: (a: unknown) => unknown) => (s: unknown): unknown => 128 + ot === "Lens" ? outer.set(g(outer.get(s)))(s) 129 + : ot === "Prism" ? (isSome(outer.preview(s)) ? outer.review(g(outer.preview(s).value)) : s) 130 + : ot === "Optional" ? (isSome(outer.getOption(s)) ? outer.set(g(outer.getOption(s).value))(s) : s) 131 + : outer.modify(g)(s) 132 + return { 133 + _tag: "Traversal", 134 + getAll: (s: unknown) => outerGetAll(s).flatMap((a) => innerGetAll(a)), 135 + modify: (f: (x: unknown) => unknown) => (s: unknown) => outerModify(innerModify(f))(s), 136 + } 137 + }
+13
src/optics/index.ts
··· 1 + /** 2 + * Optics module — composable, law-abiding accessors for immutable data. 3 + * 4 + * @module optics 5 + */ 6 + 7 + export * from "./types" 8 + export * from "./lens" 9 + export * from "./prism" 10 + export * from "./optional" 11 + export * from "./traversal" 12 + export * from "./compose" 13 + export * from "./bridges"
+78
src/optics/lens.ts
··· 1 + import type { Lens } from "./types" 2 + 3 + /** 4 + * Constructs a `Lens<S, A>` from a getter and a curried setter. 5 + * 6 + * @example 7 + * ```typescript 8 + * const nameLens = lens<User, string>( 9 + * (u) => u.name, 10 + * (name) => (u) => ({ ...u, name }), 11 + * ) 12 + * ``` 13 + */ 14 + export const lens = <S, A>( 15 + get: (s: S) => A, 16 + set: (a: A) => (s: S) => S, 17 + ): Lens<S, A> => ({ _tag: "Lens", get, set }) 18 + 19 + /** 20 + * Reads the focused value from a structure using a `Lens`. 21 + * 22 + * @example 23 + * ```typescript 24 + * view(nameLens)({ name: "Alice", age: 30 }) // "Alice" 25 + * ``` 26 + */ 27 + export const view = 28 + <S, A>(l: Lens<S, A>) => 29 + (s: S): A => 30 + l.get(s) 31 + 32 + /** 33 + * Sets the focused value in a structure, returning a new structure. 34 + * 35 + * @example 36 + * ```typescript 37 + * set(nameLens)("Bob")({ name: "Alice", age: 30 }) // { name: "Bob", age: 30 } 38 + * ``` 39 + */ 40 + export const set = 41 + <S, A>(l: Lens<S, A>) => 42 + (a: A) => 43 + (s: S): S => 44 + l.set(a)(s) 45 + 46 + /** 47 + * Modifies the focused value using a function, returning a new structure. 48 + * 49 + * @example 50 + * ```typescript 51 + * over(nameLens)((n) => n.toUpperCase())({ name: "alice", age: 30 }) 52 + * // { name: "ALICE", age: 30 } 53 + * ``` 54 + */ 55 + export const over = 56 + <S, A>(l: Lens<S, A>) => 57 + (f: (a: A) => A) => 58 + (s: S): S => 59 + l.set(f(l.get(s)))(s) 60 + 61 + /** 62 + * Builds a `Lens<S, S[K]>` for a named property of a record type. 63 + * Uses two-step currying so TypeScript can infer `S` explicitly 64 + * while inferring `K` from the key argument. 65 + * 66 + * @example 67 + * ```typescript 68 + * const ageLens = prop<User>()("age") 69 + * view(ageLens)({ name: "Alice", age: 30 }) // 30 70 + * ``` 71 + */ 72 + export const prop = 73 + <S>() => 74 + <K extends keyof S>(key: K): Lens<S, S[K]> => 75 + lens( 76 + (s) => s[key], 77 + (a) => (s) => ({ ...s, [key]: a }), 78 + )
+92
src/optics/optional.ts
··· 1 + /** 2 + * Optional optic — smart constructor and operations. 3 + * 4 + * @module optics/optional 5 + */ 6 + 7 + import type { Option } from "../prelude/option" 8 + import { isSome, some, none } from "../prelude/option" 9 + import type { Optional } from "./types" 10 + 11 + /** 12 + * Create an Optional from a getOption function and a set function. 13 + * 14 + * @example 15 + * ```typescript 16 + * const headOpt = optional( 17 + * (arr: number[]) => arr.length > 0 ? some(arr[0]) : none, 18 + * (a) => (arr) => arr.length > 0 ? [a, ...arr.slice(1)] : arr, 19 + * ) 20 + * ``` 21 + */ 22 + export const optional = <S, A>( 23 + getOptionFn: (s: S) => Option<A>, 24 + setFn: (a: A) => (s: S) => S, 25 + ): Optional<S, A> => ({ 26 + _tag: "Optional", 27 + getOption: getOptionFn, 28 + set: setFn, 29 + }) 30 + 31 + /** 32 + * Extract the Option-valued getter from an Optional. 33 + * 34 + * @example 35 + * ```typescript 36 + * getOption(headOpt)([1, 2, 3]) // some(1) 37 + * getOption(headOpt)([]) // none 38 + * ``` 39 + */ 40 + export const getOption = 41 + <S, A>(opt: Optional<S, A>) => 42 + (s: S): Option<A> => 43 + opt.getOption(s) 44 + 45 + /** 46 + * Set a focused value through an Optional. 47 + * 48 + * @example 49 + * ```typescript 50 + * setOptional(headOpt)(99)([1, 2, 3]) // [99, 2, 3] 51 + * ``` 52 + */ 53 + export const setOptional = 54 + <S, A>(opt: Optional<S, A>) => 55 + (a: A) => 56 + (s: S): S => 57 + opt.set(a)(s) 58 + 59 + /** 60 + * Modify the focused value through an Optional. 61 + * Returns s unchanged if no value is present (None case). 62 + * 63 + * @example 64 + * ```typescript 65 + * modifyOptional(headOpt)(n => n * 2)([1, 2]) // [2, 2] 66 + * modifyOptional(headOpt)(n => n * 2)([]) // [] 67 + * ``` 68 + */ 69 + export const modifyOptional = 70 + <S, A>(opt: Optional<S, A>) => 71 + (f: (a: A) => A) => 72 + (s: S): S => { 73 + const o = opt.getOption(s) 74 + return isSome(o) ? opt.set(f(o.value))(s) : s 75 + } 76 + 77 + /** 78 + * Build an Optional that focuses on the element at array index i. 79 + * Returns None if i is out of bounds; set is a no-op for out-of-bounds. 80 + * 81 + * @example 82 + * ```typescript 83 + * getOption(index(1))([10, 20, 30]) // some(20) 84 + * getOption(index(5))([1, 2]) // none 85 + * setOptional(index(0))(99)([1, 2]) // [99, 2] 86 + * ``` 87 + */ 88 + export const index = <A>(i: number): Optional<readonly A[], A> => 89 + optional( 90 + (s) => (i >= 0 && i < s.length ? some(s[i] as A) : none), 91 + (a) => (s) => (i >= 0 && i < s.length ? [...s.slice(0, i), a, ...s.slice(i + 1)] : s), 92 + )
+58
src/optics/prism.ts
··· 1 + /** 2 + * Prism smart constructor and operations. 3 + * 4 + * A Prism focuses on a value that may or may not be present 5 + * in a sum type (discriminated union). 6 + * 7 + * @module optics/prism 8 + */ 9 + 10 + import type { Option } from "../prelude/option" 11 + import type { Prism } from "./types" 12 + 13 + /** 14 + * Construct a Prism from a preview and review function. 15 + * 16 + * @param previewFn - Extracts A from S; returns None for non-matching variants 17 + * @param reviewFn - Constructs S from A 18 + * 19 + * @example 20 + * ```typescript 21 + * const circleRadius = prism<Shape, number>( 22 + * (s) => s._tag === "Circle" ? some(s.radius) : none, 23 + * (radius) => ({ _tag: "Circle", radius }), 24 + * ) 25 + * ``` 26 + */ 27 + export const prism = <S, A>( 28 + previewFn: (s: S) => Option<A>, 29 + reviewFn: (a: A) => S, 30 + ): Prism<S, A> => ({ _tag: "Prism", preview: previewFn, review: reviewFn }) 31 + 32 + /** 33 + * Extract the focused value from S using a Prism. 34 + * Returns None when S is a non-matching variant. 35 + * 36 + * @example 37 + * ```typescript 38 + * preview(circleRadius)({ _tag: "Circle", radius: 5 }) // Some(5) 39 + * preview(circleRadius)({ _tag: "Square", side: 3 }) // None 40 + * ``` 41 + */ 42 + export const preview = 43 + <S, A>(p: Prism<S, A>) => 44 + (s: S): Option<A> => 45 + p.preview(s) 46 + 47 + /** 48 + * Construct an S from an A using a Prism. 49 + * 50 + * @example 51 + * ```typescript 52 + * review(circleRadius)(5) // { _tag: "Circle", radius: 5 } 53 + * ``` 54 + */ 55 + export const review = 56 + <S, A>(p: Prism<S, A>) => 57 + (a: A): S => 58 + p.review(a)
+72
src/optics/traversal.ts
··· 1 + /** 2 + * Traversal constructors and operations. 3 + * 4 + * A Traversal focuses on zero or more values A within a structure S. 5 + * 6 + * @module optics/traversal 7 + */ 8 + 9 + import type { Traversal } from "./types" 10 + 11 + /** 12 + * Construct a Traversal from a getAll function and a modify function. 13 + * 14 + * @example 15 + * ```typescript 16 + * const wordsTraversal = traversal( 17 + * (s: string) => s.split(" "), 18 + * (f) => (s) => s.split(" ").map(f).join(" "), 19 + * ) 20 + * ``` 21 + */ 22 + export const traversal = <S, A>( 23 + getAllFn: (s: S) => readonly A[], 24 + modifyFn: (f: (a: A) => A) => (s: S) => S, 25 + ): Traversal<S, A> => ({ 26 + _tag: "Traversal", 27 + getAll: getAllFn, 28 + modify: modifyFn, 29 + }) 30 + 31 + /** 32 + * Get all focused values from a source using a Traversal. 33 + * 34 + * @example 35 + * ```typescript 36 + * getAll(each<number>())([1, 2, 3]) // => [1, 2, 3] 37 + * ``` 38 + */ 39 + export const getAll = 40 + <S, A>(trav: Traversal<S, A>) => 41 + (s: S): readonly A[] => 42 + trav.getAll(s) 43 + 44 + /** 45 + * Modify all focused values in a source using a Traversal. 46 + * 47 + * @example 48 + * ```typescript 49 + * modifyTraversal(each<number>())((n) => n * 2)([1, 2]) // => [2, 4] 50 + * ``` 51 + */ 52 + export const modifyTraversal = 53 + <S, A>(trav: Traversal<S, A>) => 54 + (f: (a: A) => A) => 55 + (s: S): S => 56 + trav.modify(f)(s) 57 + 58 + /** 59 + * Build a Traversal that focuses on every element of a readonly array. 60 + * 61 + * @example 62 + * ```typescript 63 + * const numbers = each<number>() 64 + * getAll(numbers)([1, 2, 3]) // => [1, 2, 3] 65 + * modifyTraversal(numbers)((n) => n * 2)([1, 2, 3]) // => [2, 4, 6] 66 + * ``` 67 + */ 68 + export const each = <A>(): Traversal<readonly A[], A> => 69 + traversal( 70 + (s) => s, 71 + (f) => (s) => s.map(f), 72 + )
+88
src/optics/types.ts
··· 1 + import type { Option } from "../prelude/option" 2 + 3 + /** 4 + * Lens<S, A> — focuses on a single value A that always exists in S. 5 + * 6 + * Use a Lens when you can always get a value and always set it back. 7 + * 8 + * @example 9 + * ```typescript 10 + * const nameLens: Lens<User, string> = { 11 + * _tag: "Lens", 12 + * get: (u) => u.name, 13 + * set: (name) => (u) => ({ ...u, name }), 14 + * } 15 + * ``` 16 + */ 17 + export type Lens<S, A> = { 18 + readonly _tag: "Lens" 19 + readonly get: (s: S) => A 20 + readonly set: (a: A) => (s: S) => S 21 + } 22 + 23 + /** 24 + * Prism<S, A> — focuses on a value A that may or may not be present in S. 25 + * 26 + * Use a Prism when S has multiple variants and only some contain A. 27 + * 28 + * @example 29 + * ```typescript 30 + * const numberPrism: Prism<string, number> = { 31 + * _tag: "Prism", 32 + * preview: (s) => { const n = Number(s); return isNaN(n) ? none : some(n) }, 33 + * review: (n) => String(n), 34 + * } 35 + * ``` 36 + */ 37 + export type Prism<S, A> = { 38 + readonly _tag: "Prism" 39 + readonly preview: (s: S) => Option<A> 40 + readonly review: (a: A) => S 41 + } 42 + 43 + /** 44 + * Optional<S, A> — focuses on a value A that may or may not exist in S. 45 + * 46 + * A generalisation of Lens (get may fail) and Prism (set always works). 47 + * 48 + * @example 49 + * ```typescript 50 + * const headOpt: Optional<readonly number[], number> = { 51 + * _tag: "Optional", 52 + * getOption: (s) => s.length > 0 ? some(s[0]) : none, 53 + * set: (a) => (s) => s.length > 0 ? [a, ...s.slice(1)] : s, 54 + * } 55 + * ``` 56 + */ 57 + export type Optional<S, A> = { 58 + readonly _tag: "Optional" 59 + readonly getOption: (s: S) => Option<A> 60 + readonly set: (a: A) => (s: S) => S 61 + } 62 + 63 + /** 64 + * Traversal<S, A> — focuses on zero or more values A within S. 65 + * 66 + * Use a Traversal to read all focused values or modify each one. 67 + * 68 + * @example 69 + * ```typescript 70 + * const eachItem: Traversal<readonly number[], number> = { 71 + * _tag: "Traversal", 72 + * getAll: (s) => s, 73 + * modify: (f) => (s) => s.map(f), 74 + * } 75 + * ``` 76 + */ 77 + export type Traversal<S, A> = { 78 + readonly _tag: "Traversal" 79 + readonly getAll: (s: S) => readonly A[] 80 + readonly modify: (f: (a: A) => A) => (s: S) => S 81 + } 82 + 83 + /** 84 + * Optic<S, A> — discriminated union of all four optic types. 85 + * 86 + * Use this for functions that accept any optic (e.g., compose overloads). 87 + */ 88 + export type Optic<S, A> = Lens<S, A> | Prism<S, A> | Optional<S, A> | Traversal<S, A>
+48
tests/array.test.ts
··· 117 117 const result = binarySearchNum(10)(sorted) 118 118 expect(result._tag).toBe("None") 119 119 }) 120 + 121 + it("returns None on empty sorted array", () => { 122 + const sorted = sortNum(arr([])) 123 + const result = binarySearchNum(5)(sorted) 124 + expect(result._tag).toBe("None") 125 + }) 126 + 127 + it("finds single element that matches", () => { 128 + const sorted = sortNum(arr([5])) 129 + const result = binarySearchNum(5)(sorted) 130 + expect(result._tag).toBe("Some") 131 + }) 132 + 133 + it("returns None for single element that does not match", () => { 134 + const sorted = sortNum(arr([5])) 135 + const result = binarySearchNum(3)(sorted) 136 + expect(result._tag).toBe("None") 137 + }) 138 + 139 + it("finds element in all-identical array", () => { 140 + const sorted = sortNum(arr([5, 5, 5, 5, 5])) 141 + const result = binarySearchNum(5)(sorted) 142 + expect(result._tag).toBe("Some") 143 + }) 144 + 145 + it("returns None when target is smaller than all elements", () => { 146 + const sorted = sortNum(arr([5, 10, 15])) 147 + const result = binarySearchNum(0)(sorted) 148 + expect(result._tag).toBe("None") 149 + }) 150 + 151 + it("returns None when target is larger than all elements", () => { 152 + const sorted = sortNum(arr([5, 10, 15])) 153 + const result = binarySearchNum(100)(sorted) 154 + expect(result._tag).toBe("None") 155 + }) 156 + }) 157 + 158 + describe("take and drop edge cases", () => { 159 + it("take beyond array length returns full array", () => { 160 + const result = take(100)(arr([1, 2, 3])) 161 + expect([...result]).toEqual([1, 2, 3]) 162 + }) 163 + 164 + it("drop beyond array length returns empty array", () => { 165 + const result = drop(100)(arr([1, 2, 3])) 166 + expect([...result]).toEqual([]) 167 + }) 120 168 }) 121 169 })
+8
tests/composition.test.ts
··· 27 27 ) 28 28 expect(process(5)).toBe("result: 12") 29 29 }) 30 + 31 + it("with zero functions acts as identity", () => { 32 + expect((flow as any)()(42)).toBe(42) 33 + }) 34 + 35 + it("with single function applies it", () => { 36 + expect(flow((x: number) => x * 2)(5)).toBe(10) 37 + }) 30 38 }) 31 39 32 40 describe("utilities", () => {
+92
tests/effect-basic.test.ts
··· 15 15 mapEff, 16 16 pipe, 17 17 provide, 18 + repeatEff, 19 + retry, 18 20 runPromise, 19 21 runPromiseExit, 22 + sleep, 20 23 succeed, 21 24 sync, 22 25 tapBoth, ··· 24 27 tapEffect, 25 28 tapErr, 26 29 tapError, 30 + timeout, 27 31 } from "../src/index" 28 32 29 33 describe("Effect - Basic Operations", () => { ··· 708 712 ) 709 713 expect(result).toBe(3) 710 714 expect(log).toEqual(["both1:3", "tap:3", "both2:3"]) 715 + }) 716 + }) 717 + 718 + describe("retry - edge cases", () => { 719 + it("retry(0) executes once on success", async () => { 720 + let attempts = 0 721 + const eff = sync(() => { 722 + attempts++ 723 + return 42 724 + }) 725 + const result = await runPromise(pipe(eff, retry(0))) 726 + expect(result).toBe(42) 727 + expect(attempts).toBe(1) 728 + }) 729 + 730 + it("retry(0) propagates error without retrying", async () => { 731 + let attempts = 0 732 + const exit = await runPromiseExit(pipe(attempt(() => { attempts++; throw "boom" }), retry(0))) 733 + expect(Exit.isFailure(exit)).toBe(true) 734 + expect(attempts).toBe(1) 735 + }) 736 + 737 + it("retry(3) runs initial attempt plus 3 retries when all fail", async () => { 738 + let attempts = 0 739 + const alwaysFail: Eff<number, string, unknown> = sync(() => { 740 + attempts++ 741 + return undefined as unknown as number 742 + }) 743 + // Use attempt to make it fail 744 + const failing: Eff<number, string, unknown> = flatMap(() => fail("err") as Eff<number, string, unknown>)(alwaysFail) 745 + const exit = await runPromiseExit(pipe(failing, retry(3))) 746 + expect(Exit.isFailure(exit)).toBe(true) 747 + expect(attempts).toBe(4) 748 + }) 749 + }) 750 + 751 + describe("repeatEff - edge cases", () => { 752 + it("repeatEff with negative count executes once", async () => { 753 + let count = 0 754 + const eff = sync(() => { 755 + count++ 756 + return count 757 + }) 758 + const result = await runPromise(pipe(eff, repeatEff(-5))) 759 + expect(count).toBe(1) 760 + expect(result).toBe(1) 761 + }) 762 + }) 763 + 764 + describe("timeout - edge cases", () => { 765 + it("timeout(0) fires immediately when effect is slow", async () => { 766 + const result = await runPromise(pipe(sleep(1000), timeout(0))) 767 + expect(result).toBeNull() 768 + }) 769 + 770 + it("timeout on sync effect returns value before timeout fires", async () => { 771 + const result = await runPromise(pipe(succeed(42), timeout(100))) 772 + expect(result).toBe(42) 773 + }) 774 + }) 775 + 776 + describe("bracket - edge cases", () => { 777 + it("bracket release that throws propagates as uncaught error", async () => { 778 + // release is typed Eff<void, never, R>, but sync(() => { throw }) causes a runtime throw. 779 + // The fiber runtime will surface this as a rejection. 780 + const released: string[] = [] 781 + const run = runPromiseExit( 782 + bracket( 783 + succeed("res"), 784 + (_r) => 785 + sync(() => { 786 + released.push("before-throw") 787 + throw new Error("release-boom") 788 + }) as unknown as Eff<void, never, unknown>, 789 + (_r) => succeed(1), 790 + ), 791 + ) 792 + // We expect either the promise to reject or exit to be failure due to the throw 793 + let didThrow = false 794 + try { 795 + const exit = await run 796 + // If runtime catches it, it should be a failure 797 + if (Exit.isFailure(exit)) didThrow = true 798 + } catch { 799 + didThrow = true 800 + } 801 + expect(didThrow).toBe(true) 802 + expect(released).toContain("before-throw") 711 803 }) 712 804 }) 713 805 })
+110
tests/effect-concurrency.test.ts
··· 1 1 import { describe, expect, it } from "bun:test" 2 2 import { 3 3 all, 4 + allSequential, 4 5 Exit, 5 6 fail, 6 7 flatMap, ··· 394 395 395 396 expect(result).toEqual(["a", "b", "c"]) 396 397 expect(elapsed).toBeLessThan(150) 398 + }) 399 + }) 400 + 401 + describe("race edge cases", () => { 402 + it("race where both effects fail produces Failure exit", async () => { 403 + const exit = await runPromiseExit(race(fail("e1"), fail("e2"))) 404 + expect(Exit.isFailure(exit)).toBe(true) 405 + }) 406 + }) 407 + 408 + describe("all edge cases", () => { 409 + it("all with empty array returns empty array", async () => { 410 + const result = await runPromise(all([])) 411 + expect(result).toEqual([]) 412 + }) 413 + }) 414 + 415 + describe("allSequential", () => { 416 + it("returns empty array for empty input", async () => { 417 + const result = await runPromise(allSequential([])) 418 + expect(result).toEqual([]) 419 + }) 420 + 421 + it("runs effects and collects results", async () => { 422 + const result = await runPromise(allSequential([succeed(1), succeed(2), succeed(3)])) 423 + expect(result).toEqual([1, 2, 3]) 424 + }) 425 + 426 + it("fails on first error", async () => { 427 + const exit = await runPromiseExit( 428 + allSequential([succeed(1), fail("boom"), succeed(3)]), 429 + ) 430 + expect(Exit.isFailure(exit)).toBe(true) 431 + if (Exit.isFailure(exit)) expect(exit.error).toBe("boom") 432 + }) 433 + 434 + it("preserves sequential order", async () => { 435 + const order: number[] = [] 436 + const mkEff = (n: number) => 437 + pipe( 438 + sync(() => { 439 + order.push(n) 440 + return n 441 + }), 442 + ) 443 + await runPromise(allSequential([mkEff(1), mkEff(2), mkEff(3)])) 444 + expect(order).toEqual([1, 2, 3]) 445 + }) 446 + }) 447 + 448 + describe("zip edge cases", () => { 449 + it("zip with synchronous effects returns tuple", async () => { 450 + const result = await runPromise(zip(succeed(1), succeed("a"))) 451 + expect(result).toEqual([1, "a"]) 452 + }) 453 + }) 454 + 455 + describe("fiber edge cases", () => { 456 + it("interrupt already-completed fiber still returns Success", async () => { 457 + const program = pipe( 458 + fork(succeed(42)), 459 + flatMap((fiber) => 460 + pipe( 461 + join(fiber), 462 + flatMap((value) => 463 + pipe( 464 + interruptFiber(fiber), 465 + mapEff(() => value), 466 + ), 467 + ), 468 + ), 469 + ), 470 + ) 471 + const result = await runPromise(program) 472 + expect(result).toBe(42) 473 + }) 474 + 475 + it("join a fiber twice returns the same value", async () => { 476 + const program = pipe( 477 + fork(succeed(99)), 478 + flatMap((fiber) => 479 + pipe( 480 + join(fiber), 481 + flatMap((first) => 482 + pipe( 483 + join(fiber), 484 + mapEff((second) => [first, second] as const), 485 + ), 486 + ), 487 + ), 488 + ), 489 + ) 490 + const result = await runPromise(program) 491 + expect(result[0]).toBe(99) 492 + expect(result[1]).toBe(99) 493 + }) 494 + }) 495 + 496 + describe("deep flatMap chain", () => { 497 + it("1000 flatMaps does not stack overflow", async () => { 498 + let eff = succeed(0) 499 + for (let i = 0; i < 1000; i++) { 500 + eff = pipe( 501 + eff, 502 + flatMap((n) => succeed(n + 1)), 503 + ) 504 + } 505 + const result = await runPromise(eff) 506 + expect(result).toBe(1000) 397 507 }) 398 508 }) 399 509 })
+251
tests/optics.test.ts
··· 1 + import { describe, expect, it } from "bun:test" 2 + import { 3 + lens, view, set, over, prop, 4 + prism, preview, review, 5 + optional, getOption, setOptional, modifyOptional, index, 6 + traversal, getAll, modifyTraversal, each, 7 + compose, 8 + okPrism, errPrism, somePrism, validPrism, eachTraversal, 9 + some, none, isSome, ok, err, valid, invalid, 10 + } from "../src/index" 11 + 12 + type User = { name: string; age: number } 13 + const nameLens = lens<User, string>( 14 + (u) => u.name, 15 + (name) => (u) => ({ ...u, name }), 16 + ) 17 + const ageLens = lens<User, number>( 18 + (u) => u.age, 19 + (age) => (u) => ({ ...u, age }), 20 + ) 21 + 22 + describe("Lens", () => { 23 + describe("laws", () => { 24 + it("get-set law: set(view(s))(s) deep-equals s", () => { 25 + const s1: User = { name: "Alice", age: 30 } 26 + const s2: User = { name: "Bob", age: 25 } 27 + const s3: User = { name: "", age: 0 } 28 + expect(set(nameLens)(view(nameLens)(s1))(s1)).toEqual(s1) 29 + expect(set(nameLens)(view(nameLens)(s2))(s2)).toEqual(s2) 30 + expect(set(nameLens)(view(nameLens)(s3))(s3)).toEqual(s3) 31 + }) 32 + it("set-get law: view(set(a)(s)) === a", () => { 33 + const s: User = { name: "Alice", age: 30 } 34 + expect(view(nameLens)(set(nameLens)("Bob")(s))).toBe("Bob") 35 + expect(view(nameLens)(set(nameLens)("Carol")(s))).toBe("Carol") 36 + expect(view(ageLens)(set(ageLens)(99)(s))).toBe(99) 37 + }) 38 + it("set-set law: set(b)(set(a)(s)) deep-equals set(b)(s)", () => { 39 + const s: User = { name: "Alice", age: 30 } 40 + expect(set(nameLens)("C")(set(nameLens)("B")(s))).toEqual(set(nameLens)("C")(s)) 41 + expect(set(ageLens)(5)(set(ageLens)(1)(s))).toEqual(set(ageLens)(5)(s)) 42 + }) 43 + }) 44 + describe("operations", () => { 45 + it("view extracts the focused value", () => { 46 + expect(view(nameLens)({ name: "Alice", age: 30 })).toBe("Alice") 47 + }) 48 + it("set replaces the focused value", () => { 49 + expect(set(nameLens)("Bob")({ name: "Alice", age: 30 })).toEqual({ name: "Bob", age: 30 }) 50 + }) 51 + it("over modifies the focused value with a function", () => { 52 + expect(over(nameLens)((n) => n.toUpperCase())({ name: "alice", age: 30 })) 53 + .toEqual({ name: "ALICE", age: 30 }) 54 + }) 55 + }) 56 + describe("prop()", () => { 57 + type Pt = { x: number; y: number } 58 + const xLens = prop<Pt>()("x") 59 + it("get-set law on a record type", () => { 60 + const p1: Pt = { x: 1, y: 2 } 61 + const p2: Pt = { x: 0, y: -5 } 62 + expect(set(xLens)(view(xLens)(p1))(p1)).toEqual(p1) 63 + expect(set(xLens)(view(xLens)(p2))(p2)).toEqual(p2) 64 + }) 65 + it("set-get law on a record type", () => { 66 + const p: Pt = { x: 1, y: 2 } 67 + expect(view(xLens)(set(xLens)(42)(p))).toBe(42) 68 + }) 69 + }) 70 + }) 71 + 72 + type Shape = { _tag: "Circle"; radius: number } | { _tag: "Square"; side: number } 73 + const circlePrism = prism<Shape, number>( 74 + (s) => s._tag === "Circle" ? some(s.radius) : none, 75 + (radius) => ({ _tag: "Circle", radius }), 76 + ) 77 + 78 + describe("Prism", () => { 79 + describe("laws", () => { 80 + it("preview-review law: preview(review(a)) deep-equals Some(a)", () => { 81 + expect(preview(circlePrism)(review(circlePrism)(5))).toEqual(some(5)) 82 + expect(preview(circlePrism)(review(circlePrism)(0))).toEqual(some(0)) 83 + expect(preview(circlePrism)(review(circlePrism)(100))).toEqual(some(100)) 84 + }) 85 + it("review-preview law: if preview(s) is Some(a) then review(a) deep-equals s", () => { 86 + const circles: Shape[] = [ 87 + { _tag: "Circle", radius: 5 }, 88 + { _tag: "Circle", radius: 42 }, 89 + { _tag: "Circle", radius: 0 }, 90 + ] 91 + for (const c of circles) { 92 + const opt = preview(circlePrism)(c) 93 + if (isSome(opt)) expect(review(circlePrism)(opt.value)).toEqual(c) 94 + else throw new Error("expected Some for circle shape") 95 + } 96 + }) 97 + it("preview returns None for non-matching variant", () => { 98 + expect(preview(circlePrism)({ _tag: "Square", side: 3 })).toEqual(none) 99 + }) 100 + }) 101 + }) 102 + 103 + describe("Optional", () => { 104 + const headOpt = optional<readonly number[], number>( 105 + (arr) => arr.length > 0 ? some(arr[0] as number) : none, 106 + (a) => (arr) => arr.length > 0 ? [a, ...arr.slice(1)] : arr, 107 + ) 108 + it("getOption returns Some for existing element", () => { 109 + expect(getOption(headOpt)([10, 20])).toEqual(some(10)) 110 + }) 111 + it("getOption returns None for empty array", () => { 112 + expect(getOption(headOpt)([])).toEqual(none) 113 + }) 114 + it("setOptional replaces focused element", () => { 115 + expect(setOptional(headOpt)(99)([1, 2, 3])).toEqual([99, 2, 3]) 116 + }) 117 + it("setOptional is no-op when element absent", () => { 118 + expect(setOptional(headOpt)(99)([])).toEqual([]) 119 + }) 120 + it("modifyOptional applies function to focused element", () => { 121 + expect(modifyOptional(headOpt)((n) => n * 2)([5, 6])).toEqual([10, 6]) 122 + }) 123 + it("modifyOptional is no-op when element absent", () => { 124 + expect(modifyOptional(headOpt)((n) => n * 2)([])).toEqual([]) 125 + }) 126 + describe("index()", () => { 127 + it("returns Some for in-bounds index", () => { 128 + expect(getOption(index(1))([10, 20, 30])).toEqual(some(20)) 129 + }) 130 + it("returns None for out-of-bounds index", () => { 131 + expect(getOption(index(5))([1, 2])).toEqual(none) 132 + expect(getOption(index(-1))([1, 2])).toEqual(none) 133 + }) 134 + it("sets element at in-bounds index", () => { 135 + expect(setOptional(index(0))(99)([1, 2, 3])).toEqual([99, 2, 3]) 136 + }) 137 + it("set is no-op for out-of-bounds index", () => { 138 + expect(setOptional(index(5))(99)([1, 2])).toEqual([1, 2]) 139 + }) 140 + }) 141 + }) 142 + 143 + describe("Traversal", () => { 144 + describe("each()", () => { 145 + it("getAll returns all elements", () => { 146 + expect(getAll(each<number>())([1, 2, 3])).toEqual([1, 2, 3]) 147 + }) 148 + it("getAll on empty array returns []", () => { 149 + expect(getAll(each<number>())([])).toEqual([]) 150 + }) 151 + it("modifyTraversal applies function to every element", () => { 152 + expect(modifyTraversal(each<number>())((n) => n * 2)([1, 2, 3])).toEqual([2, 4, 6]) 153 + }) 154 + }) 155 + it("custom traversal getAll/modify work on arrays", () => { 156 + const trav = traversal<string[], string>( 157 + (s) => s, 158 + (f) => (s) => s.map(f), 159 + ) 160 + expect(getAll(trav)(["a", "b"])).toEqual(["a", "b"]) 161 + expect(modifyTraversal(trav)((x) => x.toUpperCase())(["a", "b"])).toEqual(["A", "B"]) 162 + }) 163 + }) 164 + 165 + describe("compose", () => { 166 + type Outer = { inner: User } 167 + const outerNameLens = compose(prop<Outer>()("inner"), prop<User>()("name")) 168 + it("compose(lens, lens): view through two levels of nesting", () => { 169 + expect(view(outerNameLens)({ inner: { name: "Alice", age: 30 } })).toBe("Alice") 170 + }) 171 + it("compose(lens, lens): set through two levels of nesting", () => { 172 + expect(set(outerNameLens)("Bob")({ inner: { name: "Alice", age: 30 } })) 173 + .toEqual({ inner: { name: "Bob", age: 30 } }) 174 + }) 175 + it("compose(lens, prism): produces a working optional (getOption Some on match)", () => { 176 + type Container = { shape: Shape } 177 + const containerRadiusOpt = compose(prop<Container>()("shape"), circlePrism) 178 + const circle: Container = { shape: { _tag: "Circle", radius: 7 } } 179 + expect(getOption(containerRadiusOpt)(circle)).toEqual(some(7)) 180 + }) 181 + it("compose(lens, prism): produces a working optional (getOption None on non-match)", () => { 182 + type Container = { shape: Shape } 183 + const containerRadiusOpt = compose(prop<Container>()("shape"), circlePrism) 184 + const square: Container = { shape: { _tag: "Square", side: 3 } } 185 + expect(getOption(containerRadiusOpt)(square)).toEqual(none) 186 + }) 187 + it("compose(optional, prism): set works correctly (regression)", () => { 188 + const headOpt = optional<readonly Shape[], Shape>( 189 + (arr) => arr.length > 0 ? some(arr[0] as Shape) : none, 190 + (a) => (arr) => arr.length > 0 ? [a, ...arr.slice(1)] : arr, 191 + ) 192 + const composed = compose(headOpt, circlePrism) 193 + const shapes: readonly Shape[] = [{ _tag: "Circle", radius: 5 }] 194 + expect(setOptional(composed)(99)(shapes)).toEqual([{ _tag: "Circle", radius: 99 }]) 195 + }) 196 + }) 197 + 198 + describe("Bridges", () => { 199 + describe("okPrism", () => { 200 + it("preview on ok(42) returns Some(42)", () => { 201 + expect(preview(okPrism<number, string>())(ok(42))).toEqual(some(42)) 202 + }) 203 + it("preview on err returns None", () => { 204 + expect(preview(okPrism<number, string>())(err("e"))).toEqual(none) 205 + }) 206 + it("review(42) returns ok(42)", () => { 207 + expect(review(okPrism<number, string>())(42)).toEqual(ok(42)) 208 + }) 209 + }) 210 + describe("errPrism", () => { 211 + it("preview on err('x') returns Some('x')", () => { 212 + expect(preview(errPrism<number, string>())(err("x"))).toEqual(some("x")) 213 + }) 214 + it("preview on ok returns None", () => { 215 + expect(preview(errPrism<number, string>())(ok(42))).toEqual(none) 216 + }) 217 + it("review('x') returns err('x')", () => { 218 + expect(review(errPrism<number, string>())("x")).toEqual(err("x")) 219 + }) 220 + }) 221 + describe("somePrism", () => { 222 + it("preview on some(42) returns Some(42)", () => { 223 + expect(preview(somePrism<number>())(some(42))).toEqual(some(42)) 224 + }) 225 + it("preview on none returns None", () => { 226 + expect(preview(somePrism<number>())(none)).toEqual(none) 227 + }) 228 + it("review(42) returns some(42)", () => { 229 + expect(review(somePrism<number>())(42)).toEqual(some(42)) 230 + }) 231 + }) 232 + describe("validPrism", () => { 233 + it("preview on valid(42) returns Some(42)", () => { 234 + expect(preview(validPrism<number, string>())(valid(42))).toEqual(some(42)) 235 + }) 236 + it("preview on invalid(['e']) returns None", () => { 237 + expect(preview(validPrism<number, string>())(invalid(["e"]))).toEqual(none) 238 + }) 239 + it("review(42) returns valid(42)", () => { 240 + expect(review(validPrism<number, string>())(42)).toEqual(valid(42)) 241 + }) 242 + }) 243 + describe("eachTraversal", () => { 244 + it("getAll on [1,2,3] returns [1,2,3]", () => { 245 + expect(getAll(eachTraversal<number>())([1, 2, 3])).toEqual([1, 2, 3]) 246 + }) 247 + it("modifyTraversal(x => x*2) on [1,2,3] returns [2,4,6]", () => { 248 + expect(modifyTraversal(eachTraversal<number>())((x) => x * 2)([1, 2, 3])).toEqual([2, 4, 6]) 249 + }) 250 + }) 251 + })
+9
tests/option.test.ts
··· 184 184 expect(pipe(none, getOrElse(0))).toBe(0) 185 185 }) 186 186 }) 187 + 188 + describe("large array performance", () => { 189 + it("traverseOption handles 10000 items", () => { 190 + const items = Array.from({ length: 10000 }, (_, i) => i) 191 + const result = traverseOption((n: number) => some(n * 2))(items) 192 + expect(result._tag).toBe("Some") 193 + if (result._tag === "Some") expect(result.value.length).toBe(10000) 194 + }) 195 + }) 187 196 })
+28
tests/pattern-matching.test.ts
··· 54 54 expect(grade(85)).toBe("B") 55 55 expect(grade(65)).toBe("F") 56 56 }) 57 + 58 + it("with zero guards returns default", () => { 59 + const result = when(42)()(() => "default") 60 + expect(result).toBe("default") 61 + }) 62 + 63 + it("with all guards false returns default", () => { 64 + const result = when(5)( 65 + [(x) => x > 10, () => "big"], 66 + [(x) => x > 7, () => "medium"], 67 + )(() => "small") 68 + expect(result).toBe("small") 69 + }) 57 70 }) 58 71 59 72 describe("matchOn", () => { ··· 101 114 102 115 it("returns default for unmatched case", () => { 103 116 expect(describeStatus({ kind: "active" })).toBe("unknown") 117 + }) 118 + }) 119 + 120 + describe("match edge cases", () => { 121 + it("with unknown _tag throws at runtime", () => { 122 + expect(() => 123 + match({ _tag: "Unknown" } as any)({ Known: () => "x" }), 124 + ).toThrow(TypeError) 125 + }) 126 + }) 127 + 128 + describe("matchLiteral edge cases", () => { 129 + it("with value not in cases returns undefined", () => { 130 + const result = matchLiteral("missing" as any)({ present: "x" }) 131 + expect(result).toBeUndefined() 104 132 }) 105 133 }) 106 134
+9
tests/result.test.ts
··· 229 229 }) 230 230 }) 231 231 232 + describe("large array performance", () => { 233 + it("traverseResult handles 10000 items", () => { 234 + const items = Array.from({ length: 10000 }, (_, i) => i) 235 + const result = traverseResult((n: number) => ok(n * 2))(items) 236 + expect(result._tag).toBe("Ok") 237 + if (result._tag === "Ok") expect(result.value.length).toBe(10000) 238 + }) 239 + }) 240 + 232 241 describe("bimap", () => { 233 242 it("transforms Ok value with first function", () => { 234 243 const result = pipe(
+16
tests/validation.test.ts
··· 101 101 expect(result).toEqual(invalid(["error1", "error2"])) 102 102 }) 103 103 104 + it("accumulates empty error arrays when both invalid with no errors", () => { 105 + const vf = invalid<string>([]) 106 + const va = invalid<string>([]) 107 + const result = pipe(vf, apValidation(va)) 108 + expect(result).toEqual(invalid([])) 109 + }) 110 + 104 111 it("accumulates multiple errors from multiple validations", () => { 105 112 type Person = { name: string; age: number; email: string } 106 113 const makePerson = ··· 233 240 (name: string, age: number) => ({ name, age }), 234 241 ) 235 242 expect(result).toEqual(invalid(["name required", "age required"])) 243 + }) 244 + 245 + it("returns error when only second input is invalid", () => { 246 + const result = validate2( 247 + valid("ok"), 248 + invalidOne("bad"), 249 + (a: string, b: string) => a + b, 250 + ) 251 + expect(result).toEqual(invalid(["bad"])) 236 252 }) 237 253 }) 238 254