···1010- **Refinements** — Encode validated properties in the type system (`Positive`, `NonEmpty`, `Sorted`)
1111- **Result & Option** — Handle errors and nullability as values, not exceptions
1212- **Pattern Matching** — Exhaustive, type-safe matching on discriminated unions
1313+- **Optics** — Composable, law-abiding immutable data access (Lens, Prism, Optional, Traversal)
1314- **Effect System** — Composable effects with typed errors and dependency injection
1415- **Fiber Runtime** — Cooperative concurrency with cancellation, racing, and parallelism
1515-- **Zero Dependencies** — Single-file library, ~950 lines of TypeScript
1616+- **Zero Dependencies** — Pure TypeScript library with zero runtime dependencies
16171718## Installation
1819
+1
src/index.ts
···10101111export * from "./data"
1212export * from "./effect"
1313+export * from "./optics"
1314// Re-export all public API from modular structure
1415export * from "./prelude"
+68
src/optics/bridges.ts
···11+/**
22+ * Pre-built optics for purus-ts data types.
33+ *
44+ * Bridges connect the optics module to Result, Option, and Validation,
55+ * providing ready-to-use Prisms for each variant and a Traversal for arrays.
66+ *
77+ * @module optics/bridges
88+ */
99+1010+import type { Result } from "../prelude/result"
1111+import type { Option } from "../prelude/option"
1212+import type { Validation } from "../data/validation"
1313+import type { Prism, Traversal } from "./types"
1414+import { ok, err } from "../prelude/result"
1515+import { some, none } from "../prelude/option"
1616+import { valid } from "../data/validation"
1717+import { prism } from "./prism"
1818+import { traversal } from "./traversal"
1919+2020+/**
2121+ * Prism focusing on the Ok variant of a Result.
2222+ * preview extracts the success value; review wraps a value in Ok.
2323+ */
2424+export const okPrism = <T, E>(): Prism<Result<T, E>, T> =>
2525+ prism(
2626+ (r) => (r._tag === "Ok" ? some(r.value) : none),
2727+ (t) => ok(t),
2828+ )
2929+3030+/**
3131+ * Prism focusing on the Err variant of a Result.
3232+ * preview extracts the error value; review wraps a value in Err.
3333+ */
3434+export const errPrism = <T, E>(): Prism<Result<T, E>, E> =>
3535+ prism(
3636+ (r) => (r._tag === "Err" ? some(r.error) : none),
3737+ (e) => err(e),
3838+ )
3939+4040+/**
4141+ * Prism focusing on the Some variant of an Option.
4242+ * preview extracts the wrapped value; review wraps a value in Some.
4343+ */
4444+export const somePrism = <T>(): Prism<Option<T>, T> =>
4545+ prism(
4646+ (o) => (o._tag === "Some" ? some(o.value) : none),
4747+ (t) => some(t),
4848+ )
4949+5050+/**
5151+ * Prism focusing on the Valid variant of a Validation.
5252+ * preview extracts the success value; review wraps a value in Valid.
5353+ */
5454+export const validPrism = <A, E>(): Prism<Validation<A, E>, A> =>
5555+ prism(
5656+ (v) => (v._tag === "Valid" ? some(v.value) : none),
5757+ (a) => valid(a),
5858+ )
5959+6060+/**
6161+ * Traversal focusing on every element of a readonly array.
6262+ * getAll returns all elements; modify applies a function to each element.
6363+ */
6464+export const eachTraversal = <A>(): Traversal<readonly A[], A> =>
6565+ traversal(
6666+ (s) => s,
6767+ (f) => (s) => s.map(f),
6868+ )
+137
src/optics/compose.ts
···11+/**
22+ * Type-safe optic composition.
33+ *
44+ * Composition table (outer \ inner → result):
55+ *
66+ * | outer \ inner | Lens | Prism | Optional | Traversal |
77+ * |---------------|-----------|----------|----------|-----------|
88+ * | Lens | Lens | Optional | Optional | Traversal |
99+ * | Prism | Optional | Prism | Optional | Traversal |
1010+ * | Optional | Optional | Optional | Optional | Traversal |
1111+ * | Traversal | Traversal | Traversal| Traversal| Traversal |
1212+ *
1313+ * @module optics/compose
1414+ */
1515+1616+import { isSome, flatMapOption, mapOption } from "../prelude/option"
1717+import type { Lens, Optional, Prism, Traversal } from "./types"
1818+1919+// ── Most specific overloads ──────────────────────────────────────────────────
2020+2121+export function compose<S, A, B>(outer: Lens<S, A>, inner: Lens<A, B>): Lens<S, B>
2222+export function compose<S, A, B>(outer: Prism<S, A>, inner: Prism<A, B>): Prism<S, B>
2323+2424+// ── Optional-yielding overloads ──────────────────────────────────────────────
2525+2626+export function compose<S, A, B>(outer: Lens<S, A>, inner: Prism<A, B>): Optional<S, B>
2727+export function compose<S, A, B>(outer: Prism<S, A>, inner: Lens<A, B>): Optional<S, B>
2828+export function compose<S, A, B>(outer: Lens<S, A>, inner: Optional<A, B>): Optional<S, B>
2929+export function compose<S, A, B>(outer: Optional<S, A>, inner: Lens<A, B>): Optional<S, B>
3030+export function compose<S, A, B>(outer: Prism<S, A>, inner: Optional<A, B>): Optional<S, B>
3131+export function compose<S, A, B>(outer: Optional<S, A>, inner: Prism<A, B>): Optional<S, B>
3232+export function compose<S, A, B>(outer: Optional<S, A>, inner: Optional<A, B>): Optional<S, B>
3333+3434+// ── Traversal-yielding overloads (any + Traversal, Traversal + any) ──────────
3535+3636+export function compose<S, A, B>(outer: Lens<S, A>, inner: Traversal<A, B>): Traversal<S, B>
3737+export function compose<S, A, B>(outer: Prism<S, A>, inner: Traversal<A, B>): Traversal<S, B>
3838+export function compose<S, A, B>(outer: Optional<S, A>, inner: Traversal<A, B>): Traversal<S, B>
3939+export function compose<S, A, B>(outer: Traversal<S, A>, inner: Lens<A, B>): Traversal<S, B>
4040+export function compose<S, A, B>(outer: Traversal<S, A>, inner: Prism<A, B>): Traversal<S, B>
4141+export function compose<S, A, B>(outer: Traversal<S, A>, inner: Optional<A, B>): Traversal<S, B>
4242+export function compose<S, A, B>(outer: Traversal<S, A>, inner: Traversal<A, B>): Traversal<S, B>
4343+4444+// ── Implementation ───────────────────────────────────────────────────────────
4545+// eslint-disable-next-line @typescript-eslint/no-explicit-any
4646+export function compose(outer: any, inner: any): any {
4747+ const ot = outer._tag as string
4848+ const it = inner._tag as string
4949+5050+ if (ot === "Lens" && it === "Lens") {
5151+ return {
5252+ _tag: "Lens",
5353+ get: (s: unknown) => inner.get(outer.get(s)),
5454+ set: (b: unknown) => (s: unknown) => outer.set(inner.set(b)(outer.get(s)))(s),
5555+ }
5656+ }
5757+5858+ if (ot === "Prism" && it === "Prism") {
5959+ return {
6060+ _tag: "Prism",
6161+ preview: (s: unknown) => flatMapOption(inner.preview)(outer.preview(s)),
6262+ review: (b: unknown) => outer.review(inner.review(b)),
6363+ }
6464+ }
6565+6666+ // Lens + Prism → Optional
6767+ if (ot === "Lens" && it === "Prism") {
6868+ return {
6969+ _tag: "Optional",
7070+ getOption: (s: unknown) => inner.preview(outer.get(s)),
7171+ set: (b: unknown) => (s: unknown) => outer.set(inner.review(b))(s),
7272+ }
7373+ }
7474+7575+ // Prism + Lens → Optional
7676+ if (ot === "Prism" && it === "Lens") {
7777+ return {
7878+ _tag: "Optional",
7979+ getOption: (s: unknown) => mapOption(inner.get)(outer.preview(s)),
8080+ set: (b: unknown) => (s: unknown) => {
8181+ const oa = outer.preview(s)
8282+ return isSome(oa) ? outer.review(inner.set(b)(oa.value)) : s
8383+ },
8484+ }
8585+ }
8686+8787+ // Optional-yielding: covers Lens+Opt, Opt+Lens, Prism+Opt, Opt+Prism, Opt+Opt
8888+ if (ot !== "Traversal" && it !== "Traversal") {
8989+ const outerGet = (s: unknown) =>
9090+ ot === "Lens" ? { _tag: "Some" as const, value: outer.get(s) }
9191+ : (ot === "Prism" ? outer.preview(s) : outer.getOption(s))
9292+ return {
9393+ _tag: "Optional",
9494+ getOption: (s: unknown) => {
9595+ const oa = outerGet(s)
9696+ if (!isSome(oa)) return oa
9797+ return it === "Lens" ? { _tag: "Some" as const, value: inner.get(oa.value) }
9898+ : it === "Prism" ? inner.preview(oa.value)
9999+ : inner.getOption(oa.value)
100100+ },
101101+ set: (b: unknown) => (s: unknown) => {
102102+ const oa = outerGet(s)
103103+ if (!isSome(oa)) return s
104104+ const newA = it === "Prism" ? inner.review(b) : inner.set(b)(oa.value)
105105+ return ot === "Lens" ? outer.set(newA)(s)
106106+ : ot === "Prism" ? outer.review(newA) : outer.set(newA)(s)
107107+ },
108108+ }
109109+ }
110110+111111+ // Traversal-yielding: at least one side is a Traversal
112112+ const outerGetAll = (s: unknown): readonly unknown[] =>
113113+ ot === "Lens" ? [outer.get(s)]
114114+ : ot === "Prism" ? (isSome(outer.preview(s)) ? [outer.preview(s).value] : [])
115115+ : ot === "Optional" ? (isSome(outer.getOption(s)) ? [outer.getOption(s).value] : [])
116116+ : outer.getAll(s)
117117+ const innerGetAll = (a: unknown): readonly unknown[] =>
118118+ it === "Lens" ? [inner.get(a)]
119119+ : it === "Prism" ? (isSome(inner.preview(a)) ? [inner.preview(a).value] : [])
120120+ : it === "Optional" ? (isSome(inner.getOption(a)) ? [inner.getOption(a).value] : [])
121121+ : inner.getAll(a)
122122+ const innerModify = (f: (x: unknown) => unknown) => (a: unknown): unknown =>
123123+ it === "Lens" ? inner.set(f(inner.get(a)))(a)
124124+ : it === "Prism" ? (isSome(inner.preview(a)) ? inner.review(f(inner.preview(a).value)) : a)
125125+ : it === "Optional" ? (isSome(inner.getOption(a)) ? inner.set(f(inner.getOption(a).value))(a) : a)
126126+ : inner.modify(f)(a)
127127+ const outerModify = (g: (a: unknown) => unknown) => (s: unknown): unknown =>
128128+ ot === "Lens" ? outer.set(g(outer.get(s)))(s)
129129+ : ot === "Prism" ? (isSome(outer.preview(s)) ? outer.review(g(outer.preview(s).value)) : s)
130130+ : ot === "Optional" ? (isSome(outer.getOption(s)) ? outer.set(g(outer.getOption(s).value))(s) : s)
131131+ : outer.modify(g)(s)
132132+ return {
133133+ _tag: "Traversal",
134134+ getAll: (s: unknown) => outerGetAll(s).flatMap((a) => innerGetAll(a)),
135135+ modify: (f: (x: unknown) => unknown) => (s: unknown) => outerModify(innerModify(f))(s),
136136+ }
137137+}
+13
src/optics/index.ts
···11+/**
22+ * Optics module — composable, law-abiding accessors for immutable data.
33+ *
44+ * @module optics
55+ */
66+77+export * from "./types"
88+export * from "./lens"
99+export * from "./prism"
1010+export * from "./optional"
1111+export * from "./traversal"
1212+export * from "./compose"
1313+export * from "./bridges"
+78
src/optics/lens.ts
···11+import type { Lens } from "./types"
22+33+/**
44+ * Constructs a `Lens<S, A>` from a getter and a curried setter.
55+ *
66+ * @example
77+ * ```typescript
88+ * const nameLens = lens<User, string>(
99+ * (u) => u.name,
1010+ * (name) => (u) => ({ ...u, name }),
1111+ * )
1212+ * ```
1313+ */
1414+export const lens = <S, A>(
1515+ get: (s: S) => A,
1616+ set: (a: A) => (s: S) => S,
1717+): Lens<S, A> => ({ _tag: "Lens", get, set })
1818+1919+/**
2020+ * Reads the focused value from a structure using a `Lens`.
2121+ *
2222+ * @example
2323+ * ```typescript
2424+ * view(nameLens)({ name: "Alice", age: 30 }) // "Alice"
2525+ * ```
2626+ */
2727+export const view =
2828+ <S, A>(l: Lens<S, A>) =>
2929+ (s: S): A =>
3030+ l.get(s)
3131+3232+/**
3333+ * Sets the focused value in a structure, returning a new structure.
3434+ *
3535+ * @example
3636+ * ```typescript
3737+ * set(nameLens)("Bob")({ name: "Alice", age: 30 }) // { name: "Bob", age: 30 }
3838+ * ```
3939+ */
4040+export const set =
4141+ <S, A>(l: Lens<S, A>) =>
4242+ (a: A) =>
4343+ (s: S): S =>
4444+ l.set(a)(s)
4545+4646+/**
4747+ * Modifies the focused value using a function, returning a new structure.
4848+ *
4949+ * @example
5050+ * ```typescript
5151+ * over(nameLens)((n) => n.toUpperCase())({ name: "alice", age: 30 })
5252+ * // { name: "ALICE", age: 30 }
5353+ * ```
5454+ */
5555+export const over =
5656+ <S, A>(l: Lens<S, A>) =>
5757+ (f: (a: A) => A) =>
5858+ (s: S): S =>
5959+ l.set(f(l.get(s)))(s)
6060+6161+/**
6262+ * Builds a `Lens<S, S[K]>` for a named property of a record type.
6363+ * Uses two-step currying so TypeScript can infer `S` explicitly
6464+ * while inferring `K` from the key argument.
6565+ *
6666+ * @example
6767+ * ```typescript
6868+ * const ageLens = prop<User>()("age")
6969+ * view(ageLens)({ name: "Alice", age: 30 }) // 30
7070+ * ```
7171+ */
7272+export const prop =
7373+ <S>() =>
7474+ <K extends keyof S>(key: K): Lens<S, S[K]> =>
7575+ lens(
7676+ (s) => s[key],
7777+ (a) => (s) => ({ ...s, [key]: a }),
7878+ )
+92
src/optics/optional.ts
···11+/**
22+ * Optional optic — smart constructor and operations.
33+ *
44+ * @module optics/optional
55+ */
66+77+import type { Option } from "../prelude/option"
88+import { isSome, some, none } from "../prelude/option"
99+import type { Optional } from "./types"
1010+1111+/**
1212+ * Create an Optional from a getOption function and a set function.
1313+ *
1414+ * @example
1515+ * ```typescript
1616+ * const headOpt = optional(
1717+ * (arr: number[]) => arr.length > 0 ? some(arr[0]) : none,
1818+ * (a) => (arr) => arr.length > 0 ? [a, ...arr.slice(1)] : arr,
1919+ * )
2020+ * ```
2121+ */
2222+export const optional = <S, A>(
2323+ getOptionFn: (s: S) => Option<A>,
2424+ setFn: (a: A) => (s: S) => S,
2525+): Optional<S, A> => ({
2626+ _tag: "Optional",
2727+ getOption: getOptionFn,
2828+ set: setFn,
2929+})
3030+3131+/**
3232+ * Extract the Option-valued getter from an Optional.
3333+ *
3434+ * @example
3535+ * ```typescript
3636+ * getOption(headOpt)([1, 2, 3]) // some(1)
3737+ * getOption(headOpt)([]) // none
3838+ * ```
3939+ */
4040+export const getOption =
4141+ <S, A>(opt: Optional<S, A>) =>
4242+ (s: S): Option<A> =>
4343+ opt.getOption(s)
4444+4545+/**
4646+ * Set a focused value through an Optional.
4747+ *
4848+ * @example
4949+ * ```typescript
5050+ * setOptional(headOpt)(99)([1, 2, 3]) // [99, 2, 3]
5151+ * ```
5252+ */
5353+export const setOptional =
5454+ <S, A>(opt: Optional<S, A>) =>
5555+ (a: A) =>
5656+ (s: S): S =>
5757+ opt.set(a)(s)
5858+5959+/**
6060+ * Modify the focused value through an Optional.
6161+ * Returns s unchanged if no value is present (None case).
6262+ *
6363+ * @example
6464+ * ```typescript
6565+ * modifyOptional(headOpt)(n => n * 2)([1, 2]) // [2, 2]
6666+ * modifyOptional(headOpt)(n => n * 2)([]) // []
6767+ * ```
6868+ */
6969+export const modifyOptional =
7070+ <S, A>(opt: Optional<S, A>) =>
7171+ (f: (a: A) => A) =>
7272+ (s: S): S => {
7373+ const o = opt.getOption(s)
7474+ return isSome(o) ? opt.set(f(o.value))(s) : s
7575+ }
7676+7777+/**
7878+ * Build an Optional that focuses on the element at array index i.
7979+ * Returns None if i is out of bounds; set is a no-op for out-of-bounds.
8080+ *
8181+ * @example
8282+ * ```typescript
8383+ * getOption(index(1))([10, 20, 30]) // some(20)
8484+ * getOption(index(5))([1, 2]) // none
8585+ * setOptional(index(0))(99)([1, 2]) // [99, 2]
8686+ * ```
8787+ */
8888+export const index = <A>(i: number): Optional<readonly A[], A> =>
8989+ optional(
9090+ (s) => (i >= 0 && i < s.length ? some(s[i] as A) : none),
9191+ (a) => (s) => (i >= 0 && i < s.length ? [...s.slice(0, i), a, ...s.slice(i + 1)] : s),
9292+ )
+58
src/optics/prism.ts
···11+/**
22+ * Prism smart constructor and operations.
33+ *
44+ * A Prism focuses on a value that may or may not be present
55+ * in a sum type (discriminated union).
66+ *
77+ * @module optics/prism
88+ */
99+1010+import type { Option } from "../prelude/option"
1111+import type { Prism } from "./types"
1212+1313+/**
1414+ * Construct a Prism from a preview and review function.
1515+ *
1616+ * @param previewFn - Extracts A from S; returns None for non-matching variants
1717+ * @param reviewFn - Constructs S from A
1818+ *
1919+ * @example
2020+ * ```typescript
2121+ * const circleRadius = prism<Shape, number>(
2222+ * (s) => s._tag === "Circle" ? some(s.radius) : none,
2323+ * (radius) => ({ _tag: "Circle", radius }),
2424+ * )
2525+ * ```
2626+ */
2727+export const prism = <S, A>(
2828+ previewFn: (s: S) => Option<A>,
2929+ reviewFn: (a: A) => S,
3030+): Prism<S, A> => ({ _tag: "Prism", preview: previewFn, review: reviewFn })
3131+3232+/**
3333+ * Extract the focused value from S using a Prism.
3434+ * Returns None when S is a non-matching variant.
3535+ *
3636+ * @example
3737+ * ```typescript
3838+ * preview(circleRadius)({ _tag: "Circle", radius: 5 }) // Some(5)
3939+ * preview(circleRadius)({ _tag: "Square", side: 3 }) // None
4040+ * ```
4141+ */
4242+export const preview =
4343+ <S, A>(p: Prism<S, A>) =>
4444+ (s: S): Option<A> =>
4545+ p.preview(s)
4646+4747+/**
4848+ * Construct an S from an A using a Prism.
4949+ *
5050+ * @example
5151+ * ```typescript
5252+ * review(circleRadius)(5) // { _tag: "Circle", radius: 5 }
5353+ * ```
5454+ */
5555+export const review =
5656+ <S, A>(p: Prism<S, A>) =>
5757+ (a: A): S =>
5858+ p.review(a)
+72
src/optics/traversal.ts
···11+/**
22+ * Traversal constructors and operations.
33+ *
44+ * A Traversal focuses on zero or more values A within a structure S.
55+ *
66+ * @module optics/traversal
77+ */
88+99+import type { Traversal } from "./types"
1010+1111+/**
1212+ * Construct a Traversal from a getAll function and a modify function.
1313+ *
1414+ * @example
1515+ * ```typescript
1616+ * const wordsTraversal = traversal(
1717+ * (s: string) => s.split(" "),
1818+ * (f) => (s) => s.split(" ").map(f).join(" "),
1919+ * )
2020+ * ```
2121+ */
2222+export const traversal = <S, A>(
2323+ getAllFn: (s: S) => readonly A[],
2424+ modifyFn: (f: (a: A) => A) => (s: S) => S,
2525+): Traversal<S, A> => ({
2626+ _tag: "Traversal",
2727+ getAll: getAllFn,
2828+ modify: modifyFn,
2929+})
3030+3131+/**
3232+ * Get all focused values from a source using a Traversal.
3333+ *
3434+ * @example
3535+ * ```typescript
3636+ * getAll(each<number>())([1, 2, 3]) // => [1, 2, 3]
3737+ * ```
3838+ */
3939+export const getAll =
4040+ <S, A>(trav: Traversal<S, A>) =>
4141+ (s: S): readonly A[] =>
4242+ trav.getAll(s)
4343+4444+/**
4545+ * Modify all focused values in a source using a Traversal.
4646+ *
4747+ * @example
4848+ * ```typescript
4949+ * modifyTraversal(each<number>())((n) => n * 2)([1, 2]) // => [2, 4]
5050+ * ```
5151+ */
5252+export const modifyTraversal =
5353+ <S, A>(trav: Traversal<S, A>) =>
5454+ (f: (a: A) => A) =>
5555+ (s: S): S =>
5656+ trav.modify(f)(s)
5757+5858+/**
5959+ * Build a Traversal that focuses on every element of a readonly array.
6060+ *
6161+ * @example
6262+ * ```typescript
6363+ * const numbers = each<number>()
6464+ * getAll(numbers)([1, 2, 3]) // => [1, 2, 3]
6565+ * modifyTraversal(numbers)((n) => n * 2)([1, 2, 3]) // => [2, 4, 6]
6666+ * ```
6767+ */
6868+export const each = <A>(): Traversal<readonly A[], A> =>
6969+ traversal(
7070+ (s) => s,
7171+ (f) => (s) => s.map(f),
7272+ )
+88
src/optics/types.ts
···11+import type { Option } from "../prelude/option"
22+33+/**
44+ * Lens<S, A> — focuses on a single value A that always exists in S.
55+ *
66+ * Use a Lens when you can always get a value and always set it back.
77+ *
88+ * @example
99+ * ```typescript
1010+ * const nameLens: Lens<User, string> = {
1111+ * _tag: "Lens",
1212+ * get: (u) => u.name,
1313+ * set: (name) => (u) => ({ ...u, name }),
1414+ * }
1515+ * ```
1616+ */
1717+export type Lens<S, A> = {
1818+ readonly _tag: "Lens"
1919+ readonly get: (s: S) => A
2020+ readonly set: (a: A) => (s: S) => S
2121+}
2222+2323+/**
2424+ * Prism<S, A> — focuses on a value A that may or may not be present in S.
2525+ *
2626+ * Use a Prism when S has multiple variants and only some contain A.
2727+ *
2828+ * @example
2929+ * ```typescript
3030+ * const numberPrism: Prism<string, number> = {
3131+ * _tag: "Prism",
3232+ * preview: (s) => { const n = Number(s); return isNaN(n) ? none : some(n) },
3333+ * review: (n) => String(n),
3434+ * }
3535+ * ```
3636+ */
3737+export type Prism<S, A> = {
3838+ readonly _tag: "Prism"
3939+ readonly preview: (s: S) => Option<A>
4040+ readonly review: (a: A) => S
4141+}
4242+4343+/**
4444+ * Optional<S, A> — focuses on a value A that may or may not exist in S.
4545+ *
4646+ * A generalisation of Lens (get may fail) and Prism (set always works).
4747+ *
4848+ * @example
4949+ * ```typescript
5050+ * const headOpt: Optional<readonly number[], number> = {
5151+ * _tag: "Optional",
5252+ * getOption: (s) => s.length > 0 ? some(s[0]) : none,
5353+ * set: (a) => (s) => s.length > 0 ? [a, ...s.slice(1)] : s,
5454+ * }
5555+ * ```
5656+ */
5757+export type Optional<S, A> = {
5858+ readonly _tag: "Optional"
5959+ readonly getOption: (s: S) => Option<A>
6060+ readonly set: (a: A) => (s: S) => S
6161+}
6262+6363+/**
6464+ * Traversal<S, A> — focuses on zero or more values A within S.
6565+ *
6666+ * Use a Traversal to read all focused values or modify each one.
6767+ *
6868+ * @example
6969+ * ```typescript
7070+ * const eachItem: Traversal<readonly number[], number> = {
7171+ * _tag: "Traversal",
7272+ * getAll: (s) => s,
7373+ * modify: (f) => (s) => s.map(f),
7474+ * }
7575+ * ```
7676+ */
7777+export type Traversal<S, A> = {
7878+ readonly _tag: "Traversal"
7979+ readonly getAll: (s: S) => readonly A[]
8080+ readonly modify: (f: (a: A) => A) => (s: S) => S
8181+}
8282+8383+/**
8484+ * Optic<S, A> — discriminated union of all four optic types.
8585+ *
8686+ * Use this for functions that accept any optic (e.g., compose overloads).
8787+ */
8888+export type Optic<S, A> = Lens<S, A> | Prism<S, A> | Optional<S, A> | Traversal<S, A>
+48
tests/array.test.ts
···117117 const result = binarySearchNum(10)(sorted)
118118 expect(result._tag).toBe("None")
119119 })
120120+121121+ it("returns None on empty sorted array", () => {
122122+ const sorted = sortNum(arr([]))
123123+ const result = binarySearchNum(5)(sorted)
124124+ expect(result._tag).toBe("None")
125125+ })
126126+127127+ it("finds single element that matches", () => {
128128+ const sorted = sortNum(arr([5]))
129129+ const result = binarySearchNum(5)(sorted)
130130+ expect(result._tag).toBe("Some")
131131+ })
132132+133133+ it("returns None for single element that does not match", () => {
134134+ const sorted = sortNum(arr([5]))
135135+ const result = binarySearchNum(3)(sorted)
136136+ expect(result._tag).toBe("None")
137137+ })
138138+139139+ it("finds element in all-identical array", () => {
140140+ const sorted = sortNum(arr([5, 5, 5, 5, 5]))
141141+ const result = binarySearchNum(5)(sorted)
142142+ expect(result._tag).toBe("Some")
143143+ })
144144+145145+ it("returns None when target is smaller than all elements", () => {
146146+ const sorted = sortNum(arr([5, 10, 15]))
147147+ const result = binarySearchNum(0)(sorted)
148148+ expect(result._tag).toBe("None")
149149+ })
150150+151151+ it("returns None when target is larger than all elements", () => {
152152+ const sorted = sortNum(arr([5, 10, 15]))
153153+ const result = binarySearchNum(100)(sorted)
154154+ expect(result._tag).toBe("None")
155155+ })
156156+ })
157157+158158+ describe("take and drop edge cases", () => {
159159+ it("take beyond array length returns full array", () => {
160160+ const result = take(100)(arr([1, 2, 3]))
161161+ expect([...result]).toEqual([1, 2, 3])
162162+ })
163163+164164+ it("drop beyond array length returns empty array", () => {
165165+ const result = drop(100)(arr([1, 2, 3]))
166166+ expect([...result]).toEqual([])
167167+ })
120168 })
121169})
+8
tests/composition.test.ts
···2727 )
2828 expect(process(5)).toBe("result: 12")
2929 })
3030+3131+ it("with zero functions acts as identity", () => {
3232+ expect((flow as any)()(42)).toBe(42)
3333+ })
3434+3535+ it("with single function applies it", () => {
3636+ expect(flow((x: number) => x * 2)(5)).toBe(10)
3737+ })
3038 })
31393240 describe("utilities", () => {
+92
tests/effect-basic.test.ts
···1515 mapEff,
1616 pipe,
1717 provide,
1818+ repeatEff,
1919+ retry,
1820 runPromise,
1921 runPromiseExit,
2222+ sleep,
2023 succeed,
2124 sync,
2225 tapBoth,
···2427 tapEffect,
2528 tapErr,
2629 tapError,
3030+ timeout,
2731} from "../src/index"
28322933describe("Effect - Basic Operations", () => {
···708712 )
709713 expect(result).toBe(3)
710714 expect(log).toEqual(["both1:3", "tap:3", "both2:3"])
715715+ })
716716+ })
717717+718718+ describe("retry - edge cases", () => {
719719+ it("retry(0) executes once on success", async () => {
720720+ let attempts = 0
721721+ const eff = sync(() => {
722722+ attempts++
723723+ return 42
724724+ })
725725+ const result = await runPromise(pipe(eff, retry(0)))
726726+ expect(result).toBe(42)
727727+ expect(attempts).toBe(1)
728728+ })
729729+730730+ it("retry(0) propagates error without retrying", async () => {
731731+ let attempts = 0
732732+ const exit = await runPromiseExit(pipe(attempt(() => { attempts++; throw "boom" }), retry(0)))
733733+ expect(Exit.isFailure(exit)).toBe(true)
734734+ expect(attempts).toBe(1)
735735+ })
736736+737737+ it("retry(3) runs initial attempt plus 3 retries when all fail", async () => {
738738+ let attempts = 0
739739+ const alwaysFail: Eff<number, string, unknown> = sync(() => {
740740+ attempts++
741741+ return undefined as unknown as number
742742+ })
743743+ // Use attempt to make it fail
744744+ const failing: Eff<number, string, unknown> = flatMap(() => fail("err") as Eff<number, string, unknown>)(alwaysFail)
745745+ const exit = await runPromiseExit(pipe(failing, retry(3)))
746746+ expect(Exit.isFailure(exit)).toBe(true)
747747+ expect(attempts).toBe(4)
748748+ })
749749+ })
750750+751751+ describe("repeatEff - edge cases", () => {
752752+ it("repeatEff with negative count executes once", async () => {
753753+ let count = 0
754754+ const eff = sync(() => {
755755+ count++
756756+ return count
757757+ })
758758+ const result = await runPromise(pipe(eff, repeatEff(-5)))
759759+ expect(count).toBe(1)
760760+ expect(result).toBe(1)
761761+ })
762762+ })
763763+764764+ describe("timeout - edge cases", () => {
765765+ it("timeout(0) fires immediately when effect is slow", async () => {
766766+ const result = await runPromise(pipe(sleep(1000), timeout(0)))
767767+ expect(result).toBeNull()
768768+ })
769769+770770+ it("timeout on sync effect returns value before timeout fires", async () => {
771771+ const result = await runPromise(pipe(succeed(42), timeout(100)))
772772+ expect(result).toBe(42)
773773+ })
774774+ })
775775+776776+ describe("bracket - edge cases", () => {
777777+ it("bracket release that throws propagates as uncaught error", async () => {
778778+ // release is typed Eff<void, never, R>, but sync(() => { throw }) causes a runtime throw.
779779+ // The fiber runtime will surface this as a rejection.
780780+ const released: string[] = []
781781+ const run = runPromiseExit(
782782+ bracket(
783783+ succeed("res"),
784784+ (_r) =>
785785+ sync(() => {
786786+ released.push("before-throw")
787787+ throw new Error("release-boom")
788788+ }) as unknown as Eff<void, never, unknown>,
789789+ (_r) => succeed(1),
790790+ ),
791791+ )
792792+ // We expect either the promise to reject or exit to be failure due to the throw
793793+ let didThrow = false
794794+ try {
795795+ const exit = await run
796796+ // If runtime catches it, it should be a failure
797797+ if (Exit.isFailure(exit)) didThrow = true
798798+ } catch {
799799+ didThrow = true
800800+ }
801801+ expect(didThrow).toBe(true)
802802+ expect(released).toContain("before-throw")
711803 })
712804 })
713805})