Skip to content

Category Theory in PHP

Practical implementations of category theory concepts in PHP 8+.

Table of Contents

  1. Functors and Monads
  2. Comonads
  3. Monad Transformers
  4. Optics (Lenses)
  5. Free Monads and Interpreters

Functors and Monads

Functor and Monad Traits

<?php
namespace Category;
/**
* Option monad
*/
abstract class Option
{
abstract public function map(callable $f): Option;
abstract public function flatMap(callable $f): Option;
abstract public function getOrElse($default);
abstract public function isNone(): bool;
abstract public function isSome(): bool;
}
class Some extends Option
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function map(callable $f): Option
{
return new Some($f($this->value));
}
public function flatMap(callable $f): Option
{
return $f($this->value);
}
public function getOrElse($default)
{
return $this->value;
}
public function isNone(): bool
{
return false;
}
public function isSome(): bool
{
return true;
}
public function get()
{
return $this->value;
}
}
class None extends Option
{
public function map(callable $f): Option
{
return $this;
}
public function flatMap(callable $f): Option
{
return $this;
}
public function getOrElse($default)
{
return $default;
}
public function isNone(): bool
{
return true;
}
public function isSome(): bool
{
return false;
}
}
/**
* Either monad
*/
abstract class Either
{
abstract public function map(callable $f): Either;
abstract public function flatMap(callable $f): Either;
abstract public function isLeft(): bool;
abstract public function isRight(): bool;
}
class Left extends Either
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function map(callable $f): Either
{
return $this;
}
public function flatMap(callable $f): Either
{
return $this;
}
public function getValue()
{
return $this->value;
}
public function isLeft(): bool
{
return true;
}
public function isRight(): bool
{
return false;
}
}
class Right extends Either
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function map(callable $f): Either
{
return new Right($f($this->value));
}
public function flatMap(callable $f): Either
{
return $f($this->value);
}
public function getValue()
{
return $this->value;
}
public function isLeft(): bool
{
return false;
}
public function isRight(): bool
{
return true;
}
}
/**
* Example: Safe operations with Option
*/
function safeDivide(float $x, float $y): Option
{
if ($y == 0) {
return new None();
}
return new Some($x / $y);
}
function compute(): Option
{
return safeDivide(10, 2)
->flatMap(fn($x) => safeDivide($x, 5))
->map(fn($x) => $x * 2);
}
$result = compute()->getOrElse(0); // 2.0
/**
* Example: Error handling with Either
*/
function divide(float $x, float $y): Either
{
if ($y == 0) {
return new Left("Division by zero");
}
return new Right($x / $y);
}
function pipeline(): Either
{
return divide(10, 2)
->flatMap(fn($x) => divide($x, 0))
->flatMap(fn($y) => divide($y, 2));
}
$result = pipeline();
if ($result instanceof Left) {
echo "Error: " . $result->getValue(); // "Error: Division by zero"
} else {
echo "Result: " . $result->getValue();
}

Comonads

Stream Comonad

<?php
namespace Category\Comonad;
/**
* Stream Comonad - infinite list with focus
*/
class Stream
{
private $head;
private $tailFn;
public function __construct($head, callable $tailFn)
{
$this->head = $head;
$this->tailFn = $tailFn;
}
/** Extract current element */
public function extract()
{
return $this->head;
}
/** Get tail */
public function tail(): Stream
{
return ($this->tailFn)();
}
/** Duplicate into Stream of Streams */
public function duplicate(): Stream
{
return new Stream(
$this,
fn() => $this->tail()->duplicate()
);
}
/** Extend function over stream */
public function extend(callable $f): Stream
{
return new Stream(
$f($this),
fn() => $this->tail()->extend($f)
);
}
/** Take first n elements */
public function take(int $n): array
{
$result = [$this->head];
$current = $this;
for ($i = 1; $i < $n; $i++) {
$current = $current->tail();
$result[] = $current->head;
}
return $result;
}
/** Create infinite stream from iterator */
public static function iterate($initial, callable $f): Stream
{
return new Stream(
$initial,
fn() => self::iterate($f($initial), $f)
);
}
/** Create stream from array with default */
public static function from(array $array, $default): Stream
{
$index = 0;
$generator = function() use (&$index, $array, $default, &$generator) {
$index++;
$value = $array[$index] ?? $default;
return new Stream($value, $generator);
};
return new Stream($array[0] ?? $default, $generator);
}
}
/**
* Example: Moving average with Stream
*/
function movingAverage3(Stream $stream): float
{
$values = $stream->take(3);
return array_sum($values) / count($values);
}
function streamExample(): void
{
// Infinite stream of naturals
$naturals = Stream::iterate(1, fn($n) => $n + 1);
print_r($naturals->take(10));
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Moving average
$data = Stream::iterate(10.0, fn($n) => $n + 10);
$smoothed = $data->extend(fn($s) => movingAverage3($s));
print_r($smoothed->take(5));
// Time series from array
$prices = Stream::from([100, 102, 98, 105, 110], 0);
$avgPrices = $prices->extend(fn($s) => movingAverage3($s));
print_r($avgPrices->take(3));
}

Store Comonad

<?php
namespace Category\Comonad;
/**
* Store Comonad - position in space
*/
class Store
{
private $getter;
private $position;
public function __construct(callable $getter, $position)
{
$this->getter = $getter;
$this->position = $position;
}
/** Extract value at current position */
public function extract()
{
return ($this->getter)($this->position);
}
/** Peek at different position */
public function peek($pos)
{
return ($this->getter)($pos);
}
/** Move to new position */
public function seek($pos): Store
{
return new Store($this->getter, $pos);
}
/** Duplicate into Store of Stores */
public function duplicate(): Store
{
$getter = $this->getter;
return new Store(
fn($s) => new Store($getter, $s),
$this->position
);
}
/** Extend function over Store */
public function extend(callable $f): Store
{
$getter = $this->getter;
return new Store(
fn($s) => $f(new Store($getter, $s)),
$this->position
);
}
/** Get current position */
public function getPosition()
{
return $this->position;
}
}
/**
* Example: Game of Life with Store
*/
function neighbors(array $pos): array
{
[$x, $y] = $pos;
$result = [];
for ($dx = -1; $dx <= 1; $dx++) {
for ($dy = -1; $dy <= 1; $dy++) {
if ($dx !== 0 || $dy !== 0) {
$result[] = [$x + $dx, $y + $dy];
}
}
}
return $result;
}
function gameOfLifeStep(Store $grid): bool
{
$aliveNeighbors = count(array_filter(
neighbors($grid->getPosition()),
fn($pos) => $grid->peek($pos)
));
$current = $grid->extract();
if ($current && ($aliveNeighbors === 2 || $aliveNeighbors === 3)) {
return true;
}
if (!$current && $aliveNeighbors === 3) {
return true;
}
return false;
}
function storeExample(): void
{
// Glider pattern
$alive = [[1, 0], [2, 1], [0, 2], [1, 2], [2, 2]];
$initialState = function($pos) use ($alive) {
foreach ($alive as [$x, $y]) {
if ($pos[0] === $x && $pos[1] === $y) {
return true;
}
}
return false;
};
$grid = new Store($initialState, [1, 1]);
echo "Current cell: " . ($grid->extract() ? "alive" : "dead") . "\n";
// Evolve one step
$nextGen = $grid->extend(fn($g) => gameOfLifeStep($g));
echo "Next generation: " . ($nextGen->extract() ? "alive" : "dead") . "\n";
// Evolve multiple generations
$current = $grid;
for ($i = 0; $i < 5; $i++) {
$current = $current->extend(fn($g) => gameOfLifeStep($g));
echo "Generation " . ($i + 1) . ": " .
($current->extract() ? "alive" : "dead") . "\n";
}
}

Env Comonad

<?php
namespace Category\Comonad;
/**
* Env Comonad - value with environment
*/
class Env
{
private $env;
private $value;
public function __construct($env, $value)
{
$this->env = $env;
$this->value = $value;
}
/** Extract value */
public function extract()
{
return $this->value;
}
/** Get environment */
public function ask()
{
return $this->env;
}
/** Duplicate with environment */
public function duplicate(): Env
{
return new Env($this->env, $this);
}
/** Extend function over Env */
public function extend(callable $f): Env
{
return new Env($this->env, $f($this));
}
}
/**
* Example: Theming with Env
*/
const THEMES = [
'light' => [
'primaryColor' => '#007bff',
'backgroundColor' => '#ffffff',
'textColor' => '#000000',
],
'dark' => [
'primaryColor' => '#0d6efd',
'backgroundColor' => '#1a1a1a',
'textColor' => '#ffffff',
],
];
function renderWithTheme(Env $themed): string
{
$theme = $themed->ask();
$widget = $themed->extract();
$config = THEMES[$theme];
return sprintf(
'<%s style="background: %s; color: %s; border-color: %s;">%s</%s>',
$widget['type'],
$config['backgroundColor'],
$config['textColor'],
$config['primaryColor'],
$widget['content'],
$widget['type']
);
}
function envExample(): void
{
$widget = ['content' => 'Click Me', 'type' => 'button'];
$themed = new Env('dark', $widget);
$rendered = $themed->extend(fn($env) => renderWithTheme($env));
echo "Rendered: " . $rendered->extract() . "\n";
}

Monad Transformers

OptionT - Add Option to Async Operations

<?php
namespace Category\Transformer;
use Category\Option;
use Category\Some;
use Category\None;
/**
* OptionT - adds Option semantics to Promise/Generator
*/
class OptionT
{
private $inner;
public function __construct(callable $inner)
{
$this->inner = $inner;
}
public static function some($value): OptionT
{
return new OptionT(fn() => new Some($value));
}
public static function none(): OptionT
{
return new OptionT(fn() => new None());
}
public function unwrap(): Option
{
return ($this->inner)();
}
public function map(callable $f): OptionT
{
return new OptionT(function() use ($f) {
$opt = $this->unwrap();
return $opt->map($f);
});
}
public function flatMap(callable $f): OptionT
{
return new OptionT(function() use ($f) {
$opt = $this->unwrap();
if ($opt->isNone()) {
return new None();
}
return $f($opt->get())->unwrap();
});
}
public function getOrElse($default)
{
return $this->unwrap()->getOrElse($default);
}
}
/**
* Example: Database operations with OptionT
*/
function findUser(int $id): OptionT
{
// Simulate database query
if ($id === 1) {
return OptionT::some([
'id' => 1,
'name' => 'Alice',
'email' => 'alice@example.com'
]);
}
return OptionT::none();
}
function getUserEmail(int $id): OptionT
{
return findUser($id)->map(fn($user) => $user['email']);
}
function optionTExample(): void
{
$email = getUserEmail(1);
$result = $email->getOrElse('no-email@example.com');
echo "Email: $result\n";
}

StateT - Add State to Operations

<?php
namespace Category\Transformer;
/**
* StateT - adds State semantics to operations
*/
class StateT
{
private $run;
public function __construct(callable $run)
{
$this->run = $run;
}
public static function pure($value): StateT
{
return new StateT(fn($state) => [$value, $state]);
}
public static function get(): StateT
{
return new StateT(fn($state) => [$state, $state]);
}
public static function put($newState): StateT
{
return new StateT(fn($_) => [null, $newState]);
}
public static function modify(callable $f): StateT
{
return new StateT(fn($state) => [null, $f($state)]);
}
public function runState($initialState): array
{
return ($this->run)($initialState);
}
public function evalState($initialState)
{
[$value, $_] = $this->runState($initialState);
return $value;
}
public function execState($initialState)
{
[$_, $state] = $this->runState($initialState);
return $state;
}
public function map(callable $f): StateT
{
return new StateT(function($state) use ($f) {
[$value, $newState] = $this->runState($state);
return [$f($value), $newState];
});
}
public function flatMap(callable $f): StateT
{
return new StateT(function($state) use ($f) {
[$value, $newState] = $this->runState($state);
return $f($value)->runState($newState);
});
}
}
/**
* Example: Counter with state
*/
function increment(): StateT
{
return StateT::modify(function($state) {
return [
'count' => $state['count'] + 1,
'history' => array_merge($state['history'], [$state['count'] + 1]),
];
});
}
function getCount(): StateT
{
return StateT::get()->map(fn($state) => $state['count']);
}
function stateTExample(): void
{
$program = increment()
->flatMap(fn($_) => increment())
->flatMap(fn($_) => increment())
->flatMap(fn($_) => getCount());
$initialState = ['count' => 0, 'history' => []];
[$finalCount, $finalState] = $program->runState($initialState);
echo "Final count: $finalCount\n"; // 3
echo "History: " . implode(', ', $finalState['history']) . "\n"; // 1, 2, 3
}

Optics (Lenses)

Lens - Focus on Product Types

<?php
namespace Category\Optics;
/**
* Lens for accessing nested fields
*/
class Lens
{
private $getter;
private $setter;
public function __construct(callable $getter, callable $setter)
{
$this->getter = $getter;
$this->setter = $setter;
}
public static function fromKey(string $key): Lens
{
return new Lens(
fn($source) => $source[$key],
fn($source, $value) => array_merge($source, [$key => $value])
);
}
public function view($source)
{
return ($this->getter)($source);
}
public function set($source, $value)
{
return ($this->setter)($source, $value);
}
public function over($source, callable $f)
{
$oldValue = $this->view($source);
$newValue = $f($oldValue);
return $this->set($source, $newValue);
}
public function compose(Lens $other): Lens
{
return new Lens(
fn($s) => $other->view($this->view($s)),
fn($s, $b) => $this->set($s, $other->set($this->view($s), $b))
);
}
}
/**
* Example: Person with nested Address
*/
$nameLens = Lens::fromKey('name');
$ageLens = Lens::fromKey('age');
$addressLens = Lens::fromKey('address');
$cityLens = Lens::fromKey('city');
// Compose lenses for nested access
$personCityLens = $addressLens->compose($cityLens);
function lensExample(): void
{
global $nameLens, $ageLens, $personCityLens;
$person = [
'name' => 'Alice',
'age' => 30,
'address' => [
'street' => '123 Main St',
'city' => 'Boston',
'zipCode' => '02101',
],
];
// View through lens
$name = $nameLens->view($person);
echo "Name: $name\n"; // "Alice"
// Set through lens
$person2 = $ageLens->set($person, 31);
echo "New age: " . $person2['age'] . "\n"; // 31
// Modify through lens
$person3 = $nameLens->over($person, fn($n) => strtoupper($n));
echo "Uppercase name: " . $person3['name'] . "\n"; // "ALICE"
// Composed lens
$city = $personCityLens->view($person);
echo "City: $city\n"; // "Boston"
$person4 = $personCityLens->set($person, 'New York');
echo "New city: " . $person4['address']['city'] . "\n"; // "New York"
}

Prism - Focus on Sum Types

<?php
namespace Category\Optics;
/**
* Prism for accessing sum type variants
*/
class Prism
{
private $matcher;
private $builder;
public function __construct(callable $matcher, callable $builder)
{
$this->matcher = $matcher;
$this->builder = $builder;
}
public function preview($source)
{
return ($this->matcher)($source);
}
public function review($value)
{
return ($this->builder)($value);
}
public function modify($source, callable $f)
{
$matched = $this->preview($source);
if ($matched === null) {
return null;
}
return $this->review($f($matched));
}
public function compose(Prism $other): Prism
{
return new Prism(
function($s) use ($other) {
$a = $this->preview($s);
return $a === null ? null : $other->preview($a);
},
fn($b) => $this->review($other->review($b))
);
}
}
/**
* Example: Result type prisms
*/
function success($value): array
{
return ['tag' => 'success', 'value' => $value];
}
function error($err): array
{
return ['tag' => 'error', 'error' => $err];
}
$successPrism = new Prism(
fn($r) => $r['tag'] === 'success' ? $r['value'] : null,
fn($v) => success($v)
);
$errorPrism = new Prism(
fn($r) => $r['tag'] === 'error' ? $r['error'] : null,
fn($e) => error($e)
);
function prismExample(): void
{
global $successPrism, $errorPrism;
$result1 = success(42);
$result2 = error('Not found');
// Preview through prism
$value = $successPrism->preview($result1);
echo "Success value: $value\n"; // 42
$errValue = $successPrism->preview($result2);
echo "Error through success prism: " . var_export($errValue, true) . "\n"; // null
// Review (construct) through prism
$newSuccess = $successPrism->review(100);
print_r($newSuccess); // ['tag' => 'success', 'value' => 100]
// Modify through prism
$modified = $successPrism->modify($result1, fn($n) => $n * 2);
print_r($modified); // ['tag' => 'success', 'value' => 84]
}

Free Monads and Interpreters

Free Monad DSL

<?php
namespace Category\Free;
/**
* Free monad for building DSLs
*/
abstract class Free
{
abstract public function bind(callable $f): Free;
}
class Pure extends Free
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function getValue()
{
return $this->value;
}
public function bind(callable $f): Free
{
return $f($this->value);
}
}
class FreeF extends Free
{
private $fa;
public function __construct($fa)
{
$this->fa = $fa;
}
public function getFA()
{
return $this->fa;
}
public function bind(callable $f): Free
{
// Simplified implementation
return $this;
}
}
/**
* File operation DSL
*/
class FileOp
{
public string $tag;
public string $path;
public $content;
public $next;
public static function read(string $path, callable $next): array
{
return [
'tag' => 'read',
'path' => $path,
'next' => $next,
];
}
public static function write(string $path, string $content, $next): array
{
return [
'tag' => 'write',
'path' => $path,
'content' => $content,
'next' => $next,
];
}
public static function delete(string $path, $next): array
{
return [
'tag' => 'delete',
'path' => $path,
'next' => $next,
];
}
}
/**
* DSL constructors
*/
function readFile(string $path): Free
{
return new FreeF(FileOp::read($path, fn($content) => new Pure($content)));
}
function writeFile(string $path, string $content): Free
{
return new FreeF(FileOp::write($path, $content, new Pure(null)));
}
function deleteFile(string $path): Free
{
return new FreeF(FileOp::delete($path, new Pure(null)));
}
/**
* Example program
*/
function copyFileProgram(string $src, string $dest): Free
{
return new FreeF(
FileOp::read($src, function($content) use ($dest) {
return new FreeF(
FileOp::write($dest, $content, new Pure(null))
);
})
);
}
/**
* Interpreter - execute in console
*/
function interpretConsole(Free $program)
{
if ($program instanceof Pure) {
return $program->getValue();
}
if ($program instanceof FreeF) {
$op = $program->getFA();
switch ($op['tag']) {
case 'read':
echo "Reading from: {$op['path']}\n";
$content = "Content of {$op['path']}";
return interpretConsole($op['next']($content));
case 'write':
echo "Writing to: {$op['path']}\n";
echo "Content: {$op['content']}\n";
return interpretConsole($op['next']);
case 'delete':
echo "Deleting: {$op['path']}\n";
return interpretConsole($op['next']);
}
}
}
function freeMonadExample(): void
{
$program = copyFileProgram('input.txt', 'output.txt');
interpretConsole($program);
}

Additional Examples

Bifunctor for Arrays

<?php
namespace Category;
/**
* Bifunctor operations on arrays (key, value)
*/
class ArrayBifunctor
{
public static function bimap(array $arr, callable $fKey, callable $fValue): array
{
$result = [];
foreach ($arr as $key => $value) {
$newKey = $fKey($key);
$newValue = $fValue($value);
$result[$newKey] = $newValue;
}
return $result;
}
public static function first(array $arr, callable $f): array
{
return self::bimap($arr, $f, fn($v) => $v);
}
public static function second(array $arr, callable $f): array
{
return self::bimap($arr, fn($k) => $k, $f);
}
}
function bifunctorExample(): void
{
$data = ['a' => 1, 'b' => 2, 'c' => 3];
// Map over both keys and values
$mapped = ArrayBifunctor::bimap(
$data,
fn($k) => strtoupper($k),
fn($v) => $v * 2
);
print_r($mapped); // ['A' => 2, 'B' => 4, 'C' => 6]
// Map only values
$values = ArrayBifunctor::second($data, fn($v) => $v + 10);
print_r($values); // ['a' => 11, 'b' => 12, 'c' => 13]
}

End of PHP Category Theory Implementations