An embedded DSL that makes categorical structure explicit and manipulable in TypeScript/JavaScript.
Core Concepts | Practical payoffs | Usage | Blog post
Category Theory for Programmers by Bartosz Milewski is excellent but leans heavily on Haskell. This project bridges that gap for the JS/TS ecosystem.
The goal isn't to write category theory proofs in production code. It's to develop intuition for compositional patterns you already use dailyβand to know when they'll compose predictably, and when they won't.
- Monads aren't magicβthey're what you get when you have a round-trip (adjunction) and compose the two functors
- Promise.then is flatMap, and the reason it "feels right" is because it satisfies the monad laws
- The laws aren't arbitraryβthey're the triangle identities of the underlying adjunction, bubbling up
- Why async/await "infects" your codebase (hint: it's an imperfect adjunction)
interface Morphism<A, B> {
source: string; // for debugging/visualization
target: string;
apply: (a: A) => B;
}
const compose = <A, B, C>(
g: Morphism<B, C>,
f: Morphism<A, B>
): Morphism<A, C> => ({
source: f.source,
target: g.target,
apply: (a) => g.apply(f.apply(a))
});interface Functor<F> {
fmap: <A, B>(f: Morphism<A, B>) => Morphism<F<A>, F<B>>;
}
const ArrayFunctor: Functor<Array> = {
fmap: (f) => ({
source: `Array<${f.source}>`,
target: `Array<${f.target}>`,
apply: (arr) => arr.map(f.apply)
})
};interface Adjunction<F, G> {
left: Functor<F>; // F: C β D (left adjoint)
right: Functor<G>; // G: D β C (right adjoint)
unit: <A>(a: A) => G<F<A>>; // Ξ·: A β G(F(A))
counit: <B>(fgb: F<G<B>>) => B; // Ξ΅: F(G(B)) β B
}const monadFromAdjunction = <F, G>(adj: Adjunction<F, G>) => ({
// M<A> = G<F<A>>
pure: <A>(a: A) => adj.unit(a),
join: <A>(mma: G<F<G<F<A>>>>): G<F<A>> =>
adj.right.fmap({ apply: adj.counit }).apply(mma),
flatMap: <A, B>(ma: G<F<A>>, f: (a: A) => G<F<B>>) =>
join(adj.right.fmap(adj.left.fmap({ apply: f })).apply(ma))
});function processUser(id: string) {
const user = getUser(id);
if (user === null) return null;
const profile = getProfile(user.profileId);
if (profile === null) return null;
return formatOutput(user, profile);
}const processUser = (id: string) =>
getUser(id)
.flatMap(user => getProfile(user.profileId))
.map(profile => formatOutput(profile));The monad laws guarantee safe refactoring. You're relying on mathematics, not convention.
src/
βββ core/ # Core categorical abstractions
β βββ morphism.ts # Morphism type, composition, identity
β βββ functor.ts # Functor interface (Array, Option, Identity)
β βββ natural.ts # Natural transformations (head, last, flatten, etc.)
β βββ adjunction.ts # Adjunction interface and triangle identities
β βββ monad.ts # Monad (Array, Option, Promise) + Kleisli composition
β βββ yoneda.ts # Yoneda lemma, Coyoneda, Continuations (CPS)
β βββ index.ts
βββ instances/ # Additional monad implementations
β βββ either.ts # Either<E, A> - typed error handling (Left/Right)
β βββ reader.ts # Reader<R, A> - dependency injection pattern
β βββ state.ts # State<S, A> - stateful computations
β βββ writer.ts # Writer<W, A> - logging/accumulation with Monoid
β βββ io.ts # IO<A> - encapsulating side effects
β βββ index.ts
βββ laws/ # Law verification functions
β βββ functor-laws.ts # Identity and composition laws
β βββ monad-laws.ts # Left/right identity, associativity
β βββ natural-laws.ts # Naturality condition
β βββ adjunction-laws.ts # Triangle identities
β βββ index.ts
βββ examples/ # Practical demonstrations
β βββ option-chaining.ts # Safe navigation, Kleisli composition
β βββ async-composition.ts # Promise composition, TaskEither, retry
β βββ state-monad.ts # Counter, stack, RNG, game state, parser
β βββ index.ts
βββ index.ts # Main entry point with all exports
npm install
npm run build
npm testimport {
// Core
morphism, compose, identity, pipe,
// Option monad
some, none, isSome, OptionMonad,
// Either monad
left, right, isRight, EitherMonad,
// Other monads
ReaderMonad, StateMonad, IOMonad,
// Law verification
checkFunctorLaws, checkMonadLaws,
} from 'category-theory-js';
// Create morphisms with explicit types
const double = morphism('number', 'number', (x: number) => x * 2);
const toString = morphism('number', 'string', (x: number) => `Value: ${x}`);
// Compose them (right-to-left, mathematical style)
const doubleAndStringify = compose(toString, double);
console.log(doubleAndStringify.apply(21)); // "Value: 42"
// Option monad - safe null handling
const safeDivide = (a: number, b: number) =>
b === 0 ? none : some(a / b);
const result = OptionMonad.flatMap(
OptionMonad.flatMap(some(10), x => safeDivide(x, 2)),
x => safeDivide(x, 5)
);
console.log(result); // { _tag: 'Some', value: 1 }
// Either monad - typed error handling
const parseNumber = (s: string) => {
const n = parseFloat(s);
return isNaN(n) ? left('Not a number') : right(n);
};
const parsed = EitherMonad.flatMap(
parseNumber('42'),
n => right(n * 2)
);
console.log(parsed); // { _tag: 'Right', right: 84 }| Monad | Type | Use Case |
|---|---|---|
| Option | Option<A> |
Nullable values, safe property access |
| Either | Either<E, A> |
Typed error handling, validation |
| Reader | Reader<R, A> |
Dependency injection, configuration |
| State | State<S, A> |
Stateful computations, parsing |
| Writer | Writer<W, A> |
Logging, accumulating output |
| IO | IO<A> |
Side effects, lazy evaluation |
| Promise | Promise<A> |
Async operations (built-in) |
| Array | A[] |
Non-determinism, multiple results |
import {
checkFunctorLaws,
checkMonadLaws,
verifyNaturality,
checkAdjunctionLaws,
} from 'category-theory-js';
// Verify functor laws for Array
const functorResult = checkFunctorLaws(
'Array',
{ fmap: (arr, f) => arr.map(f) },
[
{ value: [1, 2, 3], f: (x: number) => x * 2, g: (x: number) => x + 1 }
]
);
console.log(functorResult.passed); // true
// Verify monad laws for Option
const monadResult = checkMonadLaws(
'Option',
OptionMonad,
[{
value: 5,
monadicValue: some(5),
f: (x: number) => some(x * 2),
g: (x: number) => some(x + 1),
}]
);
console.log(monadResult.passed); // true
// Results include: Left Identity, Right Identity, Associativity- Left Identity:
pure(a).flatMap(f) === f(a) - Right Identity:
m.flatMap(pure) === m - Associativity:
m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
The Yoneda lemma is one of the most profound results in category theory. In programming terms:
import {
toYoneda, fromYoneda, yonedaMap,
cont, contMap, contFlatMap, runCont,
demonstrateFusion,
} from 'category-theory-js';
// Yoneda gives you FREE functor mapping through function composition
// Multiple maps fuse into a single traversal:
const result = demonstrateFusion(
[1, 2, 3],
x => x * 2, // double
x => x + 1, // increment
x => x * x // square
);
// Without fusion: 3 traversals
// With Yoneda: 1 traversal with composed function (x => ((x * 2) + 1)Β²)
console.log(result.isEqual); // true
// Continuations ARE Yoneda (for Identity functor)
// Cont<A> = βR. (A β R) β R
const computation = contFlatMap(
contFlatMap(cont(10), x => cont(x * 2)),
x => cont(x + 5)
);
console.log(runCont(computation)); // 25Why it matters:
- CPS is Yoneda: Continuation-passing style is the Yoneda embedding
- Fusion: Multiple
mapcalls become one traversal - Coyoneda: Get a functor instance for ANY type, for free
- Category Theory for Programmers - Bartosz Milewski
- fp-ts - Functional programming in TypeScript
- Effect - A powerful effect system for TypeScript
- Professor Frisby's Mostly Adequate Guide