Skip to content

Instantly share code, notes, and snippets.

@kindgracekind
Created May 12, 2025 14:59
Show Gist options
  • Save kindgracekind/767a5c4da3ce5f346bb909f577ccf7e4 to your computer and use it in GitHub Desktop.
Save kindgracekind/767a5c4da3ce5f346bb909f577ccf7e4 to your computer and use it in GitHub Desktop.
Programming with needles
import { type, entity, literal } from "./needles.js";
// Schema:
// Person:
// - has_one Name: string
// - has_one Age: number
// - has_many Post
// Post:
// - has_one Title: string
// - has_one Content: string
// Declare types
const Person = type();
const Name = type();
const Age = type();
const Post = type();
const Title = type();
const Content = type();
// Create entities
const alice = entity(Person);
alice.set(Name, literal("Alice"));
alice.set(Age, literal(30));
const bob = entity(Person);
bob.set(Name, literal("Bob"));
bob.set(Age, literal(20));
const post = entity(Post);
post.set(Title, literal("My Post"));
post.set(Content, literal("This is my first post!"));
alice.append(Post, post);
const post2 = entity(Post);
post2.set(Title, literal("My Post 2"));
post2.set(Content, literal("This is my second post!"));
alice.append(Post, post2);
// Value access
console.log(alice.last(Post).get(Content).to); // -> "This is my second post"
class Needle {
constructor({
from = null,
to = null,
child = null,
next = null,
label = null,
} = {}) {
this.label = label;
this.from = from;
this.to = to;
this.child = child;
this.next = next;
}
get lastChild() {
if (!this.child) {
return null;
}
let curr = this.child;
while (curr.next) {
curr = curr.next;
}
return curr;
}
addChild(needle) {
if (this.child) {
this.lastChild.next = needle;
} else {
this.child = needle;
}
needle.from = this;
}
findChildBy(fn) {
if (!this.child) {
return null;
}
let curr = this.child;
while (curr) {
if (fn(curr)) {
return curr;
}
curr = curr.next;
}
return null;
}
getOrCreatePeg(type) {
let peg = this.getPeg(type);
if (!peg) {
peg = new Needle({ from: this, to: type });
this.addChild(peg);
}
return peg;
}
getPeg(type) {
return this.findChildBy((child) => child.to === type);
}
get(type) {
return this.getPeg(type)?.child?.to;
}
set(type, val) {
const peg = this.getOrCreatePeg(type);
const link = new Needle({ from: peg, to: val });
peg.child = link;
}
append(type, value) {
const peg = this.getOrCreatePeg(type);
const link = new Needle({ from: peg, to: value });
peg.addChild(link);
}
first(type) {
return this.getPeg(type)?.child?.to;
}
last(type) {
return this.getPeg(type)?.lastChild?.to;
}
find(type, fn) {
const peg = this.getPeg(type);
if (!peg) {
return null;
}
return peg.findChildBy((child) => fn(child.to))?.to;
}
}
class Domain {
constructor() {
this.root = new Needle({ label: "Root" });
this.literalType = this.type("Literal");
}
type(label) {
const needle = new Needle({ label });
needle.to = needle; // types point to themselves
return needle;
}
entity(type, label) {
const needle = new Needle({ from: type, label });
this.root.append(type, needle); // allow global access
return needle;
}
literal(value) {
let inst = this.root.find(this.literalType, (l) => l.to === value); // re-use existing instances
if (!inst) {
inst = this.entity(this.literalType, value);
inst.to = value; // literals point to their value
}
return inst;
}
}
// Direct functions for convenience
const domain = new Domain();
export const type = domain.type.bind(domain);
export const entity = domain.entity.bind(domain);
export const literal = domain.literal.bind(domain);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment