An open API service indexing awesome lists of open source software.

https://github.com/ibrahimcesar/category-theory-for-the-javascript-typescript-developers

πŸ”’ Category Theory for JavaScript/TypeScript developers. An embedded DSL making functors, monads, and adjunctions explicit and manipulable.
https://github.com/ibrahimcesar/category-theory-for-the-javascript-typescript-developers

adjunctions category-theory category-theory-for-programmers composition dsl educational fp-ts functional-programming functor javascript monad typescript

Last synced: about 2 months ago
JSON representation

πŸ”’ Category Theory for JavaScript/TypeScript developers. An embedded DSL making functors, monads, and adjunctions explicit and manipulable.

Awesome Lists containing this project

README

          

# πŸ”’ Category Theory for the JavaScript Developer

**An embedded DSL that makes categorical structure explicit and manipulable in TypeScript/JavaScript.**

[Core Concepts](#core-concepts) | [Practical payoffs](#practical-payoffs) | [Usage](#usage) | [Blog post](https://ibrahimcesar.cloud/blog/category-theory-for-javascript-typescript-developers)

## Why?

[Category Theory for Programmers](https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/) 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.

## What You'll Learn

- **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)

## Core Concepts

### Morphisms as First-Class Values

```typescript
interface Morphism {
source: string; // for debugging/visualization
target: string;
apply: (a: A) => B;
}

const compose = (
g: Morphism,
f: Morphism

): Morphism
=> ({
source: f.source,
target: g.target,
apply: (a) => g.apply(f.apply(a))
});
```

### Functors as Explicit Mappings

```typescript
interface Functor {
fmap:
(f: Morphism) => Morphism, F>;
}

const ArrayFunctor: Functor = {
fmap: (f) => ({
source: `Array<${f.source}>`,
target: `Array<${f.target}>`,
apply: (arr) => arr.map(f.apply)
})
};
```

### Adjunctions: Where Monads Come From

```typescript
interface Adjunction {
left: Functor; // F: C β†’ D (left adjoint)
right: Functor; // G: D β†’ C (right adjoint)

unit: (a: A) => G>; // Ξ·: A β†’ G(F(A))
counit: (fgb: F>) => B; // Ξ΅: F(G(B)) β†’ B
}
```

### Monads Emerge Naturally

```typescript
const monadFromAdjunction = (adj: Adjunction) => ({
// M
= G>
pure:
(a: A) => adj.unit(a),

join: (mma: G>>>): G> =>
adj.right.fmap({ apply: adj.counit }).apply(mma),

flatMap: (ma: G>, f: (a: A) => G>) =>
join(adj.right.fmap(adj.left.fmap({ apply: f })).apply(ma))
});
```

## Practical Payoffs

### Before: Nested Null Checks

```typescript
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);
}
```

### After: Monadic Composition

```typescript
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.

## Project Structure

```
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 - typed error handling (Left/Right)
β”‚ β”œβ”€β”€ reader.ts # Reader - dependency injection pattern
β”‚ β”œβ”€β”€ state.ts # State - stateful computations
β”‚ β”œβ”€β”€ writer.ts # Writer - logging/accumulation with Monoid
β”‚ β”œβ”€β”€ io.ts # IO
- 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
```

## Installation

```bash
npm install
npm run build
npm test
```

## Usage

```typescript
import {
// 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 }
```

## Available Monads

| Monad | Type | Use Case |
|-------|------|----------|
| **Option** | `Option
` | Nullable values, safe property access |
| **Either** | `Either` | Typed error handling, validation |
| **Reader** | `Reader` | Dependency injection, configuration |
| **State** | `State` | Stateful computations, parsing |
| **Writer** | `Writer` | Logging, accumulating output |
| **IO** | `IO
` | Side effects, lazy evaluation |
| **Promise** | `Promise
` | Async operations (built-in) |
| **Array** | `A[]` | Non-determinism, multiple results |

## The Laws (Testable!)

```typescript
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
```

### The Three Monad Laws

1. **Left Identity**: `pure(a).flatMap(f) === f(a)`
2. **Right Identity**: `m.flatMap(pure) === m`
3. **Associativity**: `m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))`

## Yoneda Lemma

The Yoneda lemma is one of the most profound results in category theory. In programming terms:

```typescript
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
= βˆ€R. (A β†’ R) β†’ R

const computation = contFlatMap(
contFlatMap(cont(10), x => cont(x * 2)),
x => cont(x + 5)
);
console.log(runCont(computation)); // 25
```

**Why it matters:**
- **CPS is Yoneda**: Continuation-passing style is the Yoneda embedding
- **Fusion**: Multiple `map` calls become one traversal
- **Coyoneda**: Get a functor instance for ANY type, for free

## Further Reading

- [Category Theory for Programmers](https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/) - Bartosz Milewski
- [fp-ts](https://gcanti.github.io/fp-ts/) - Functional programming in TypeScript
- [Effect](https://effect.website/) - A powerful effect system for TypeScript
- [Professor Frisby's Mostly Adequate Guide](https://mostly-adequate.gitbook.io/mostly-adequate-guide/)

## Related

- [Blog post: Category Theory for JavaScript/TypeScript Developers](https://ibrahimcesar.cloud/blog/category-theory-for-javascript-typescript-developers)

## License

[MIT](./LICENSE)