writings/4/27/2024

(A Cosmic Journey with miniKanren and Effect)

Blast Off

Ever wanted your TypeScript to feel like a starship, warping through solutions instead of trudging step by step? That's the promise of **miniKanren**, a tiny relational programming language. Today we'll embark on a cosmic tour to implement a miniKanren flavor in TypeScript using the `Effect` library. Pack your space snacks!

Why miniKanren?

miniKanren lets you describe relationships rather than procedures. Instead of saying *how* to reach a result, you declare the logical connections, then let the system hunt through the possibility space. It's like asking the onboard computer for all routes to Mars and getting them instantly—no need to hand‑calculate every trajectory.

Setting the Scene

We'll use `Effect` (from [effect‑ts](https://github.com/Effect-TS)), which provides a friendly way to compose and run effects. For a logic engine we want nondeterminism—a fancy word for "try many paths at once". Here's a tiny sketch of our types: ```ts import { Effect, succeed, fail, flatMap } from "@effect/core"; // A logic variable is just a symbol tagged with a type class Var<T> { constructor(readonly name: string) {} } // A state is a simple substitution mapping variables to values interface State { readonly bindings: Map<string, unknown>; } ``` `Effect` helps us chain computations that might produce multiple states. We'll represent a **Goal** as a function from one state to a stream of states: ```ts type Goal = (s: State) => Effect<never, never, Iterable<State>>; ``` `callFresh` generates a new variable and hands it to a goal constructor: ```ts let counter = 0; const callFresh = (f: (v: Var<unknown>) => Goal): Goal => (s) => f(new Var("_" + counter++))(s); ```

Unification Station

Unification is the heart of miniKanren. We attempt to make two terms equal under a given state. If they clash, the path fails; otherwise we produce a new state with updated bindings. Here's a cheeky simplified unifier: ```ts const unify = (a: any, b: any, s: State): Effect<never, never, State> => { if (a instanceof Var) { return succeed({ bindings: new Map(s.bindings).set(a.name, b) }); } if (b instanceof Var) { return succeed({ bindings: new Map(s.bindings).set(b.name, a) }); } return a === b ? succeed(s) : fail(undefined); }; ``` A goal that asserts two terms are equal looks like this: ```ts const eq = (a: any, b: any): Goal => (s) => flatMap(() => unify(a, b, s), succeed([s])); ``` We can now compose goals with conjunction (`both`) and disjunction (`either`): ```ts const both = (g1: Goal, g2: Goal): Goal => (s) => flatMap(g1(s), (states) => succeed(Array.from(states).flatMap((ns) => g2(ns)))); const either = (g1: Goal, g2: Goal): Goal => (s) => succeed([...g1(s), ...g2(s)]); ```

Running the Adventure

Finally we want to "run" a goal and retrieve a list of possible variable bindings. ```ts const run = (n: number, g: Goal): Effect<never, never, Iterable<State>> => g({ bindings: new Map() }).map((states) => Array.from(states).slice(0, n)); ``` Let's solve a simple puzzle: find an `x` such that `x` equals `1` **or** `2`. ```ts const demo = callFresh((x) => either(eq(x, 1), eq(x, 2))); run(5, demo).unsafeRunPromise().then(console.log); // → [{ x: 1 }, { x: 2 }] ```

Wrapping Up

By mixing `Effect` with miniKanren's ideas, we get a playful way to express logic programs in TypeScript. This little journey barely scratches the surface—imagine exploring lists, relations, and even building tiny interpreters! With relational programming your code can boldly search where none has searched before. Now grab your space helmet and start exploring.