Files
ujsx/docs/research/category-theory-graph.md
2026-05-18 14:17:33 +00:00

5.0 KiB

Gategory theory as a graph

Some time ago I had a conversation with Gemini 2.5 pro, I believe, since they have backend access to youtube. The subject was regarding a specific series of videos that described category theory in basic terms that just about any dev familiar basic types and type theory could probably understand. I noticed that the relationships form a graph so after some back and forth about that I asked them to provide a sketch using graphology

import { MultiGraph, type Attributes } from "npm:graphology"

// --- Setup and Types ---

interface ObjectAttributes extends Attributes {
  name: string;
  description?: string;
}

interface MorphismAttributes extends Attributes {
  label: string;
  implementation: (input: any) => any;
  composedOf?: [string, string]; // Stores the history of the composition
}

const categoryGraph = new MultiGraph<ObjectAttributes, MorphismAttributes>();

// --- The Compose Function ---

/**
 * Composes two morphisms (g after f) and adds the resulting morphism to the graph.
 * @param fKey The key of the first morphism (A -> B).
 * @param gKey The key of the second morphism (B -> C).
 * @returns The key of the new composite morphism (A -> C).
 */
function compose(fKey: string, gKey: string): string {
  // 1. & 2. Find edges and their attributes
  if (!categoryGraph.hasEdge(fKey) || !categoryGraph.hasEdge(gKey)) {
    throw new Error('One or both morphism keys do not exist in the graph.');
  }
  const f_attributes = categoryGraph.getEdgeAttributes(fKey);
  const g_attributes = categoryGraph.getEdgeAttributes(gKey);

  const f_source = categoryGraph.source(fKey);
  const f_target = categoryGraph.target(fKey);
  const g_source = categoryGraph.source(gKey);
  const g_target = categoryGraph.target(gKey);

  // 3. Validate the path
  if (f_target !== g_source) {
    throw new Error(
      `Cannot compose: Target of '${fKey}' (${f_target}) does not match source of '${gKey}' (${g_source}).`
    );
  }

  // 4. Create the composite morphism attributes
  const compositeAttributes: MorphismAttributes = {
    label: `${g_attributes.label}${f_attributes.label}`,
    implementation: (x) => g_attributes.implementation(f_attributes.implementation(x)),
    composedOf: [fKey, gKey],
  };

  // 5. Add the new edge to the graph
  const compositeKey = `${gKey}_o_${fKey}`;
  categoryGraph.addEdgeWithKey(compositeKey, f_source, g_target, compositeAttributes);

  // 6. Return the new key
  console.log(`Successfully composed morphisms. New morphism created with key: '${compositeKey}'`);
  return compositeKey;
}

// --- Example Usage ---

// Add Objects (Nodes)
categoryGraph.addNode('Person', { name: 'Person' });
categoryGraph.addNode('Integer', { name: 'Integer' });
categoryGraph.addNode('Boolean', { name: 'Boolean' });

// Add Base Morphisms (Edges)
categoryGraph.addEdgeWithKey('age', 'Person', 'Integer', {
  label: 'Age',
  implementation: (person: { name: string; age: number }) => person.age,
});

categoryGraph.addEdgeWithKey('isVoter', 'Integer', 'Boolean', {
  label: 'isVoter?',
  implementation: (age: number) => age >= 18,
});

console.log('Graph before composition:', categoryGraph.edges());
// Expected: ['age', 'isVoter']

// Perform composition: isVoter ∘ age
const canVoteKey = compose('age', 'isVoter');

console.log('---');
console.log('Graph after composition:', categoryGraph.edges());
// Expected: ['age', 'isVoter', 'isVoter_o_age']
console.log('---');

// Let's test our new composite morphism!
const canVoteMorphism = categoryGraph.getEdgeAttributes(canVoteKey);

const alice = { name: 'Alice', age: 30 };
const bob = { name: 'Bob', age: 16 };

console.log(`Does Alice have voting rights? ${canVoteMorphism.implementation(alice)}`); // Expected: true
console.log(`Does Bob have voting rights? ${canVoteMorphism.implementation(bob)}`);   // Expected: false

// Let's test the validation by trying an invalid composition
try {
    compose('isVoter', 'age');
} catch (e) {
    console.error('---');
    console.error(`Caught expected error: ${e.message}`);
}

exporting the graph via categoryGraph.export() shows

{
  options: { type: "mixed", multi: true, allowSelfLoops: true },
  attributes: {},
  nodes: [
    { key: "Person", attributes: { name: "Person" } },
    { key: "Integer", attributes: { name: "Integer" } },
    { key: "Boolean", attributes: { name: "Boolean" } }
  ],
  edges: [
    {
      key: "age",
      source: "Person",
      target: "Integer",
      attributes: { label: "Age", implementation: [Function: implementation] }
    },
    {
      key: "isVoter",
      source: "Integer",
      target: "Boolean",
      attributes: { label: "isVoter?", implementation: [Function: implementation] }
    },
    {
      key: "isVoter_o_age",
      source: "Person",
      target: "Boolean",
      attributes: {
        label: "isVoter? ∘ Age",
        implementation: [Function: implementation],
        composedOf: [ "age", "isVoter" ]
      }
    }
  ]
}

Having the implementations actually inside the graph makes it non-serializable but that shows the basic idea.