Status: Draft v1.0 (covers all files present as of 2025‑08‑30) Scope: Functional primitives (Either/Optional, Box/canonicalization), collection utilities, caching helpers, a composable “sponge” accumulator, object guards, and an abstract entangling mechanism.
- Ergonomic FP primitives for error handling (
Either,Optional) and referential wrappers (Box), with predictable runtime semantics and strong TypeScript types. - Deterministic collection transforms that work uniformly across arrays and sets, with clear behavior around identity, duplicates, and order.
- Composability over inheritance (e.g.,
sponge,collect,Entangler). - Total/“unital” map interfaces that avoid
undefinedby construction when modeling optional lookups (OptionalMap,OptionalWeakMap). - Side‑effect boundaries explicit and narrow (e.g., caching helpers).
functional/
self.ts (identity)
sponge.ts (progressive accumulator)
guards/
object.guard.ts (isObject type guard)
optional/
either.ts (Either ADT + ops)
optional.ts (Optional = Either<NotFound, T>)
collections/
optional.map.ts (OptionalMap<K,V>)
optional.map.weak.ts (OptionalWeakMap<K extends object, V>)
optional.map.unital.ts(UnitalOptionalMap<K,V>)
collections/
collection.ts (Collection<T, I> wrapper over Array/Set)
collect.ts (helper ctor)
map.unital.ts (UnitalMap<K,V> baseline interface)
monads/
box.ts (Box<T> wrapper, isBoxed, unbox)
box.canonicalizer.ts (canonicalize via WeakMap)
top.ts (Top<T> placeholder)
references/
entangler.ts (abstract Entangler<T,B> with OptionalMap)
cache.ts (cache(store).aside compute-if-absent helper)
memo.ts (commented-out memoize prototype)
External runtime deps: none. TS target: ES2020+ recommended (WeakMap, Symbols).
- Error throwing is used for programmer‑errors / misuse (e.g., “getOnlyElement” on empty or multi‑element collections). Recoverable absence is modeled as
Optional<T>. - “Unital” interfaces guarantee a total
getthat never returnsundefinedfor absence; instead they return an identity element (here:Optional.none). - Optional is defined as
Either<NotFound, T>; absence is value‑typed (notnull/undefined), enabling exhaustiveness with helpers. - Box is a wrapper object keyed by a private
Symbol, not a nominal class;isBoxedonly tests for the presence of this symbol on an object.
export const self = <T>(x: T): T => x;- Semantics: Identity combinator.
- Complexity: O(1).
- Use: Higher‑order composition, default mappers.
export const sponge = <Pores extends object, Result>(
pores: Pores,
squeeze: (filled: Pores) => Result
) => {
// returns absorb: (water: Partial<Pores>) => Result | absorb
};-
Intent: Incrementally “absorb” pieces of an object until all declared “pores” (keys of
pores) are filled. When full, applysqueezeto the accumulated object and return itsResult. Until then, return the sameabsorbfunction for further feeding. -
Inputs:
pores: shape prototype (only its keys are used; initial values are ignored).squeeze: computation executed once all pores are filled (i.e., every key inporesis present in the internal accumulator object).
-
Returned function:
absorb(water: Partial<Pores>): Result | typeof absorb- Merges
waterinto internalsponge. - If full after merge ⇒ returns
squeeze(sponge as Pores). - Else ⇒ returns
absorb(re-entrant).
- Merges
-
Key invariants:
- Keys are considered “filled” if
key in sponge(value may beundefined). - Later calls may overwrite earlier values (last write wins).
- Keys are considered “filled” if
-
Examples:
const fillUser = sponge({ id: 0, name: "" }, ({ id, name }) => ({ id, name })); const step1 = fillUser({ id: 42 }); // returns absorb const user = step1({ name: "Ada" }); // returns { id: 42, name: "Ada" }
-
Complexity: Each call merges via
Object.assign: O(k) in number of properties provided.
export const isObject = (value: unknown): value is object =>
value !== null && typeof value === 'object';- Semantics: True for arrays, plain objects, functions with
typeof 'function'? No—functions are nottypeof 'object'; this guard returns false for functions. - Caveat: If function values should be considered “object-like” in some modules, update accordingly or add
isObjectOrFunction.
ADT:
export type Either<L,R> =
| { tag: 'Left'; left: L }
| { tag: 'Right'; right: R };Constructors & predicates:
left<L, R = never>(l: L): Either<L, R>
right<R, L = never>(r: R): Either<L, R>
isLeft<L, R>(e: Either<L,R>): e is { tag:'Left'; left:L }
isRight<L, R>(e: Either<L,R>): e is { tag:'Right'; right:R }Combinators:
getOrElse<L,R>(e, defaultVal: R): R
orElse<L,R>(e, f: (l: L) => R): R
orElseThrow<L,R,E extends Error>(e, errorFn: (l: L) => E): R
fold<L,R,T>(e, onLeft: (l:L)=>T, onRight: (r:R)=>T): T
map<L,R,LL,RR>(e, fnLeft: (l:L)=>LL, fnRight: (r:R)=>RR): Either<LL,RR>
mapLeft<L,R,LL>(e, f:(l:L)=>LL): Either<LL,R>
mapRight<L,R,RR>(e, f:(r:R)=>RR): Either<L,RR>
chain<L,R,RR>(e, f:(r:R)=> Either<L,RR>): Either<L,RR>
tapLeft<L,R>(e, fn:(l:L)=>void): Either<L,R>
tapRight<L,R>(e, fn:(r:R)=>void): Either<L,R>
tap<L,R>(e, fnLeft:(l:L)=>void, fnRight:(r:R)=>void): Either<L,R>- Laws: Standard Right‑bias.
mapRight/chainact only on Right.mapLeftacts only on Left. - Error boundaries:
orElseThrowthrows only onLeftusing lazily created error.
export const NotFound: unique symbol = Symbol('NotFound');
export type TNotFound = typeof NotFound;
export type Optional<T> = Either<TNotFound, T>;
export const none = <T>(): Optional<T> => left<TNotFound>(NotFound);
export const some = <T>(v: T): Optional<T> => right<T>(v);
export const isFound = <T>(o: Optional<T>): o is Either<never, T> => isRight(o);
export const isDefined = <T>(o: T | undefined): o is T => o !== undefined;
export const nullable = <T>(o: T | undefined): Optional<T> =>
isDefined(o) ? some(o) : none<T>();- Semantics:
Optional<T>models presence/absence withoutnull/undefined. - Interop:
nullablebridgesT | undefinedintoOptional<T>. - Pattern: Prefer
orElse/orElseThrowfromEitherto consume.
Unital concept: A “unital” map never returns undefined. Its get has an identity value for absence.
export interface UnitalMap<K, V> {
delete(key: K): boolean;
get (key: K): V;
has (key: K): boolean;
set (key: K, value: V): this;
}- Semantics: Minimal “Map‑like” surface with total
get.
export interface UnitalOptionalMap<K, V> extends UnitalMap<K, Optional<V>> {}- Semantics: Specialization where
getreturnsOptional<V>.
export class OptionalMap<K, V>
extends Map<K, Optional<V>>
implements UnitalOptionalMap<K, V> {
get(key: K): Optional<V> {
return super.get(key) ?? none<V>();
}
}- Semantics: Concrete
Mapwith totalget. - Complexity: As native
Map.
export class OptionalWeakMap<K extends object, V>
extends WeakMap<K, Optional<V>>
implements UnitalOptionalMap<K, V> {
get(key: K): Optional<V> {
return super.get(key) ?? none<V>();
}
}- Semantics: Weakly keyed optional map; keys must be objects; supports GC of entries when keys become unreachable.
Types:
export type CollectionType<T> = T[] | Set<T>;
export class Collection<T, I extends CollectionType<T> = CollectionType<T>> { /*…*/ }
export const collect = <T>(iterable: CollectionType<T>): Collection<T, CollectionType<T>>;Constructor:
new Collection<T, I>(iterable: I)Core transforms:
map<K>(f: (value: T) => K): Collection<K, Set<K>>
filter(f: (value: T) => boolean): Collection<T, Set<T>>
reduce<K>(f: (total: K, value: T) => K, initial: K): K
forEach(f: (value: T) => void): void
every(f: (value: T) => boolean): boolean
some (f: (value: T) => boolean): boolean
without(set: Set<T>): Collection<T, Set<T>>
flatten<K>(): Collection<K, Set<K>> // when elements are Collection<K,…>Mutation helpers:
add(value: T): void // push if array, add if set
delete(value: T): void// splice if array, delete if set
has(value: T): boolean
clone(): Collection<T, I> // shallow copy; preserves underlying kind (Array vs Set)
pour(set: Collection<T>): this // adds all items into 'set' via set.add(v)
getOnlyElement(): T // throws if size != 1
size(): number
end(): I // returns underlying iterable (live reference)
[Symbol.iterator](): IterableIterator<T>Semantics & invariants:
-
mapandfilteralways return aSet<…>, thus:- Order is lost.
- Duplicates are removed.
- This is intentional but surprising if the input is an array: document at call sites when order/duplication matters.
-
flattenrequires that every element is an instance ofCollection. If any element isn’t, it throws during the validation pass.-
Current implementation pattern:
if(!this.isNestedIterable<K>()) throw ...
Note:
isNestedIterablethrows on first non‑Collectionelement and then returnstrue. The throw is the only failure path.
-
-
clonepreserves the shape of the underlying iterable:Set⇒ newSet,Array⇒ new array, with elements shallow‑copied via iteration/spread. -
end()exposes the live underlying iterable; subsequent mutations via class methods affect it directly.
Complexity:
map/filterO(n).reduce/forEachO(n).add/delete/hascomplexity depends on underlying structure (ArrayO(n) for delete/has;SetO(1) average).flattenO(n + total nested sizes).
Examples:
const xs = collect([1,2,2,3]);
const ys = xs.map(x => x % 2); // Set {1,0}
const only3 = collect(new Set([3])).getOnlyElement(); // 3Sharp edges and guidance:
- If you need array semantics preserved, add helpers like
mapToArray,filterToArray, or expose a generictoArray()conversion step. pourmutates the target collection; intended for fluent pipelines.
const BOX = Symbol('box');
export type Box<T> = Record<typeof BOX, T>;
export const box = <T>(value: T): Box<T> => ({ [BOX]: value });
export const unbox = <T>(b: Box<T>): T => b[BOX];
export const isBoxed = (value: any): value is Box<any> => isObject(value) && BOX in value;-
Semantics:
Box<T>is a wrapper object with a single symbol‑keyed property holdingT. -
isBoxeddetects if an object has theBOXsymbol; primitives are never “boxed” unless created bybox. -
Purposes:
- Attach an identity to values (e.g., canonicalization).
- Distinguish wrapped from unwrapped at runtime.
-
Caveat: Because
boxreturns a fresh object, boxing a frozen object is fine (unlike designs that mark the original).
import { box, Box, isBoxed } from "./box";
import { isObject } from "../guards/object.guard";
const cannon = new WeakMap<object, Box<any>>();
export function canonicalize<T>(value: T | Box<T>): Box<T> {
if (isBoxed(value)) return value;
if (isObject(value)) {
if (!cannon.has(value)) cannon.set(value, box(value));
return cannon.get(value)!;
}
// primitives: box freshly each time
return box(value);
}-
Semantics:
- Idempotent for boxes: already boxed ⇒ return as‑is.
- Objects: stable per‑object canonical boxing via internal
WeakMap; the same object reference yields the sameBox. - Primitives: no stable identity across calls; each call returns a fresh
Box.
-
Memory behavior:
WeakMapallows entries to be GC’d when the object key becomes unreachable. -
Use cases: de‑duplication of object identities, binding tables keyed by canonical boxes, reference‑equality caches.
enum Types { Top, Bottom, None, Some, Number, String, Boolean, Collection, Object, Function, Optional }
export class Top<T> {
constructor(private readonly value: T, private readonly type: Types = Types.Top) {}
}- Status: Placeholder for a type‑tagged lattice/top element.
- Current behavior: Opaque container; unused elsewhere.
- Future direction (non‑normative): Could model a simple type domain for partial evaluation or introspection (top/bottom/constructors).
export abstract class Entangler<T, B> {
protected readonly bindings = new OptionalMap<T, B>();
/** Tear down for 'set' then rebuild for 'set'. */
public entangle(set: Collection<T>): void {
this.unbind(set);
this.bind(set);
}
/** Lookup-or-throw for whatever B is in this mode. */
public dereference(key: T): B {
return orElseThrow(this.bindings.get(key), () => new Error(`No entanglement for ${key}`));
}
protected abstract unbind(set: Collection<T>): void;
protected abstract bind (set: Collection<T>): void;
}-
Semantics:
bindings: internalOptionalMap<T,B>for established bindings.entangle(set): template method; clears/rebuilds bindings according to subclass policy.dereference(key): total accessor by design; throws if missing.
-
Subclass contract (normative):
bind(set): for eachTinset, compute andbindings.set(t, some(binding)).unbind(set): undo prior side effects and/orbindings.delete(t)for members ofset. Must be idempotent.
-
Typical usages:
- Connecting UI nodes to DOM elements, reactive variables to subscriptions, etc.
import { UnitalOptionalMap } from "./optional/collections/optional.map.unital";
import { Optional } from "./optional/optional"
export const cache = <K, V>(store: UnitalOptionalMap<K, V>) => {
const hit = (key: K): Optional<V> => store.get(key);
const probe = (key: K): boolean => store.has(key);
const cache = (key: K, value: Optional<V>): void => void store.set(key, value);
const aside = (lookup: K, value: Optional<V>): Optional<V> => {
if (!probe(lookup)) {
cache(lookup, value);
}
return hit(lookup);
};
return {
// hit,
// probe,
// cache,
aside
};
}-
Semantics (
aside): Compute‑if‑absent helper that conditionally seedsstoreatlookupwithvalueif no entry exists, then returns the entry from the store.valueis already anOptional<V>(pre‑computed); no thunking.
-
Notes:
hit,probe, and internalcacheare intentionally not exported from the factory (commented out).- Works with any
UnitalOptionalMap(OptionalMap,OptionalWeakMap, custom).
-
Sharp edge: Because
valueis computed before the check,asidemay do unnecessary work if the key already exists. See §6 forasideBy(thunked) suggestion.
Commented reference implementation of argument‑tuple memoization using a Map<string, ReturnType<T>> and JSON.stringify(args) as the key.
- Status: Not part of the public API.
- Future: See §6 for a robust, typed
memoizewith pluggable keying.
-
Collections return Sets for
map/filter. Callers MUST NOT rely on input ordering or duplication preservation in results of these transforms. -
Collection.flattenrequires nestedCollectioninstances. It throws if any element isn’t aCollection(check happens eagerly viaisNestedIterable). -
Boxidentity:- Boxes compare by reference equality;
box(1) !== box(1). canonicalize(object)returns a stableBoxfor that object reference only.
- Boxes compare by reference equality;
-
OptionalMap/OptionalWeakMapdefaults:getalways returns anOptional<V>;hasindicates whether a key is present regardless ofsomevsnonepayload stored. -
Entangler.dereferencethrows on missing binding. Subclasses SHOULD ensureentangle()populates all required keys prior to dereferencing.
| API | Time (average) | Space notes | ||
|---|---|---|---|---|
Collection.map/filter |
O(n) | Output Set of size ≤ n | ||
Collection.reduce/forEach/every/some |
O(n) | |||
Collection.add/delete/has (Array) |
O(n) | index & splice | ||
Collection.add/delete/has (Set) |
O(1) | |||
Collection.flatten |
O(n + Σ | child | ) | Output Set |
canonicalize(object) |
O(1) | Entry lives in WeakMap | ||
cache(...).aside |
O(1) | Optional map entry |
-
Thunked cache: avoid eager computation in
aside.export const cacheBy = <K,V>(store: UnitalOptionalMap<K,V>) => { const asideBy = (lookup: K, compute: () => Optional<V>): Optional<V> => { if (!store.has(lookup)) store.set(lookup, compute()); return store.get(lookup); }; return { asideBy }; };
-
Collectionarray‑preserving variants:mapToArray,filterToArray, or atoArray()conversion before mapping when order matters.
-
Collection.flattenergonomics: accept iterables (Iterable<T>) and/or union discriminate via duck typing rather thaninstanceof Collection. -
isObjectvariant for functions:isObjectLikereturning true for functions too, if needed byisBoxedor others. -
Stable box for primitives (optional): a global
Mapper primitive could provide stable boxes when required; keep off by default for memory. -
Entanglerconvenience hooks:protected set(t: T, b: B)helper writingsome(b).protected unset(t: T)helper deleting.
-
memoizeutility: pluggable keyer, WeakMap path for single object arg, TTL option.
import { Optional, some, none, isFound } from "./optional/optional";
import { orElse } from "./optional/either";
function parseIntOpt(s: string): Optional<number> {
const n = Number.parseInt(s, 10);
return Number.isNaN(n) ? none<number>() : some(n);
}
const port = orElse(parseIntOpt(process.env.PORT ?? ""), () => 3000);import { OptionalMap } from "./optional/collections/optional.map";
import { cache } from "./cache";
import { some } from "./optional/optional";
const m = new OptionalMap<string, number>();
const c = cache(m);
// First insert occurs because "a" is absent:
c.aside("a", some(42)); // -> Right(42)
// Second call reads existing (no overwrite):
c.aside("a", some(7)); // -> Right(42)import { canonicalize } from "./monads/box.canonicalizer";
import { isBoxed, unbox } from "./monads/box";
const obj = { x: 1 };
const b1 = canonicalize(obj);
const b2 = canonicalize(obj);
b1 === b2; // true
isBoxed(b1); // true
unbox(b1) === obj; // trueimport { collect } from "./collections/collect";
const out = collect([1,2,2,3,4])
.filter(x => x % 2 === 0) // Set {2,4}
.map(x => x * 10) // Set {20,40}
.end(); // returns Set<number>import { sponge } from "./functional/sponge";
type User = { id: number; name: string; email: string };
const absorbUser = sponge<User, string>({ id: 0, name: "", email: "" },
u => `${u.id}:${u.name}<${u.email}>`
);
const s1 = absorbUser({ id: 1 });
const s2 = s1({ name: "Ada" });
const formatted = s2({ email: "ada@example.com" }); // "1:Ada<ada@example.com>"import { Entangler } from "./references/entangler";
import { Collection } from "./collections/collection";
import { some } from "./optional/optional";
type NodeId = number;
type Binding = { cleanup(): void };
class MyEntangler extends Entangler<NodeId, Binding> {
protected unbind(set: Collection<NodeId>): void {
for (const id of set) {
const b = this.bindings.get(id);
if (b.tag === "Right") b.right.cleanup();
this.bindings.delete(id);
}
}
protected bind(set: Collection<NodeId>): void {
for (const id of set) {
const binding = /* ... */ { cleanup(){/*...*/} };
this.bindings.set(id, some(binding));
}
}
}-
getOnlyElementthrows:"Expected only one element, but found more than one."if ≥2."Expected one element, but found none."if 0.
-
Collectionunsupported type: throws"Unsupported iterable type"for unknowniterablekinds or inclone. -
Entangler.dereferencethrows"No entanglement for ${key}"when absent. -
spongehas no explicit errors; incorrectly shaped inputs merely delay “fullness”.
- All combinators are fully generic;
OptionalandEitherpreserve type parameters throughmap/chain. Collection<T, I>carries the underlying iterable typeIthrough methods that preserve it (clone,end,size,add,delete,has).- Methods that change structure (
map,filter,flatten) explicitly returnSet<…>wrapped inCollection.
-
Either/Optional: exhaustiveness tests for
mapLeft/mapRight/chain,orElseThrow. -
OptionalMap/OptionalWeakMap:
getreturnsnoneon unknown key;hassemantics; GC behavior (WeakMap) cannot be asserted directly—rely on type constraints and no strong references. -
Collection:
map/filterdeduplication and order loss verified.flattenthrows for mixed/flats; succeeds for nested.cloneindependence from original reference.
-
Box/canonicalize:
- Idempotence for
isBoxed. - Stability for the same object; instability for primitives across calls.
- Idempotence for
-
Sponge: partial feeds return the same function; final feed calls
squeezeexactly once. -
Entangler: subclass binds/unbinds idempotently;
dereferencebehavior.
JSON.stringifykeys (in the draftmemo.ts) can leak PII in debug tooling and are brittle for non‑serializable args; prefer custom keyers.end()exposing internal iterables allows external mutation; this is intentional. Callers MUST treat the return as mutable and avoid aliasing surprises.spongeusesObject.assign; last write wins silently. If you need write‑once semantics, wrap it and assert!(key in sponge)before write.
-
cacheBy(thunked compute, §6). -
Collectionconversions:toArray(): T[]; toSet(): Set<T>;
-
Collectioncreation helpers:from<T>(iter: Iterable<T>): Collection<T, Set<T>> // or overloads
-
Optionalhelpers:map<T,U>(o: Optional<T>, f: (t:T)=>U): Optional<U> flatMap<T,U>(o: Optional<T>, f: (t:T)=> Optional<U>): Optional<U> withDefault<T>(o: Optional<T>, d: T): T
-
isObjectLiketo include functions when desired:export const isObjectLike = (v: unknown): v is object | Function => (typeof v === 'function') || (v !== null && typeof v === 'object');
-
Treat current modules as stable except:
monads/top.ts(experimental).memo.ts(commented prototype).
-
Semver policy recommendation:
- Patch: Internal fixes; no behavioral change.
- Minor: Additive APIs (e.g., thunked cache, collection converters).
- Major: Behavior changes (e.g., making
mappreserve arrays) or renames.
- Unital map: Map whose
getis total (neverundefined) by returning an identity element for absence. - Canonicalization (Box): Assigning a stable wrapper identity to object references.
- Sponge: Progressive builder that produces a result only when a predeclared set of fields has been provided.
-
In
collection.ts,isNestedIterablereturnstrueunconditionally but may throw during iteration. This is acceptable but slightly awkward. A clearer shape:private assertNested<K>(): asserts this is Collection<Collection<K, CollectionType<K>>, CollectionType<Collection<K, CollectionType<K>>>> { for (const item of this.iterable) { if (!(item instanceof Collection)) throw new Error('Expected nested iterable, but found flat iterable.'); } } // then in flatten<K>(): this.assertNested<K>(); ...
-
In
box.canonicalizer.ts, thecannonvariable name appears to be a typo; recommendcanonorcanonicalTable.
import { collect } from "./collections/collect";
import { some, none, Optional } from "./optional/optional";
import { OptionalMap } from "./optional/collections/optional.map";
import { canonicalize } from "./monads/box.canonicalizer";
// Collections
const evens = collect([1,2,3,4]).filter(n => n % 2 === 0).end(); // Set {2,4}
// Optional maps & caching
const m = new OptionalMap<string, number>();
m.set("answer", some(42));
m.get("missing"); // none<number>()
// Canonical boxes
const o = {};
const b = canonicalize(o); // stable box for 'o'