Skip to content

Instantly share code, notes, and snippets.

@nathanclevenger
Forked from sam-lippert/README.md
Created April 8, 2025 12:35
Show Gist options
  • Save nathanclevenger/131af88fd8584c907dc62c3d2e24f221 to your computer and use it in GitHub Desktop.
Save nathanclevenger/131af88fd8584c907dc62c3d2e24f221 to your computer and use it in GitHub Desktop.
A fully-functional and extensible metamodeled graph data system with state machines, events, and atomic facts as executable functions. Inspired by ORM and lambda calculus.

exec-symbols

A purely functional DSL for modeling facts, entities, constraints, and state machines in JavaScript—using lambda-calculus–inspired Church encodings and composable building blocks.

This library showcases how to represent boolean logic, pairs, lists, entities, relationships, constraints, events, and more all as functional closures. It may be useful for educational purposes, rule engines, or domain-specific language experiments.


Table of Contents


Features

  • Church Booleans (TRUE, FALSE, AND, OR, NOT) and combinators (IF)
  • Church-encoded Pairs and Lists (pair, fst, snd, nil, cons, fold, map, append)
  • Entities with a monadic interface (Entity, unit, bind, get_id)
  • Relationship Types (RelType) supporting arity, verb function, reading, and constraints
  • Curried Verb Facts to dynamically build relationships by supplying arguments (makeVerbFact)
  • Symbolic Facts (FactSymbol) and accessors (get_verb_symbol, get_entities)
  • Events for time-based fact processing
  • State Machines with transitions, guard functions, and event-driven updates
  • Constraints (alethic vs. deontic), with predicates that evaluate over a “population”
  • Violations to track when constraints are broken
  • DSL for domain meta-facts (e.g., roles, fact types, constraint references, etc.)

Installation

If you plan to use it in a Node.js project:

npm install exec-symbols
// exec-symbols.js
// ───────────── Lambda Calculus Primitives ─────────────
const TRUE = t => f => t
const FALSE = t => f => f
const IF = b => t => e => b(t)(e)
const AND = p => q => p(q)(FALSE)
const OR = p => q => p(TRUE)(q)
const NOT = p => p(FALSE)(TRUE)
const pair = a => b => f => f(a)(b)
const fst = p => p((a, b) => a)
const snd = p => p((a, b) => b)
const nil = c => n => n
const cons = h => t => c => n => c(h)(t(c)(n))
const map = f => l => l((h, t) => cons(f(h))(t))(nil)
const fold = f => acc => l => l(f)(acc)
const append = l1 => l2 => fold(cons)(l2)(l1)
// ───────────── Utilities ─────────────
const equals = a => b => get_id(a) === get_id(b)
const nth = n => list => {
let i = 0
return list((h, t) => {
if (i === n) return h
i++
return t
})(null)
}
const mapIndex = list => {
const out = []
let i = 0
list((h, t) => {
out.push(unit(i))
i++
return t
})(null)
return out.reduceRight((acc, v) => cons(v)(acc), nil)
}
const reorder = (entities, order) =>
map(i => nth(i)(entities))(order)
// ───────────── Entities ─────────────
const Entity = id => s => s(id)
const unit = id => Entity(id)
const bind = e => f => e(id => f(id))
const get_id = e => e(id => id)
// ───────────── Readings ─────────────
const make_reading = (verb, order, template) =>
s => s(verb)(order)(template)
const get_reading_verb = r => r((v, o, t) => v)
const get_reading_order = r => r((v, o, t) => o)
const get_reading_template = r => r((v, o, t) => t)
// ───────────── RelType (no inverseReadings) ─────────────
const RelType = arity => verbFn => reading => constraints =>
s => s(arity)(verbFn)(reading)(constraints)
const get_arity = rt => rt((a, v, r, c) => a)
const get_verb = rt => rt((a, v, r, c) => v)
const get_reading = rt => rt((a, v, r, c) => r)
const get_constraints = rt => rt((a, v, r, c) => c)
// ───────────── Executable Facts ─────────────
const makeVerbFact = relType => {
const arity = get_arity(relType)
const verb = get_verb(relType)
const curry = (args, n) =>
n === 0
? verb(args)
: arg => curry(append(args)(cons(arg)(nil)), n - 1)
return curry(nil, arity)
}
const FactSymbol = verb => entities => s => s(verb)(entities)
const get_verb_symbol = f => f((v, e) => v)
const get_entities = f => f((v, e) => e)
// ───────────── Events with Readings (look up inverses externally) ─────────────
const Event = fact => time => readings => s => s(fact)(time)(readings)
const get_fact = e => e((f, t, r) => f)
const get_time = e => e((f, t, r) => t)
const get_event_readings = e => e((f, t, r) => r)
// Inverse readings must be provided externally (queried by the caller)
const emit_event = (fact, time, inverseReadings = nil) => {
const relType = get_reltype(fact)
const original = get_entities(fact)
const primary = make_reading(
get_verb_symbol(fact),
mapIndex(original),
get_reading(relType)
)
return Event(fact)(time)(cons(primary)(inverseReadings))
}
// ───────────── State Machine ─────────────
const unit_state = a => s => pair(a)(s)
const bind_state = m => f => s => {
const result = m(s)
const a = fst(result)
const s_ = snd(result)
return f(a)(s_)
}
const make_transition = guard => compute_next =>
state => input =>
IF(guard(state)(input))(
compute_next(state)(input)
)(
state
)
const unguarded = make_transition((_s => _i => TRUE))
const StateMachine = transition => initial => s => s(transition)(initial)
const run_machine = machine => stream =>
machine((transition, initial) =>
fold(event => state =>
transition(state)(get_fact(event))
)(initial)(stream)
)
const run_entity = machine => stream =>
run_machine(machine)(stream)
// ───────────── Constraints & Violations ─────────────
const ALETHIC = 'alethic'
const DEONTIC = 'deontic'
const Constraint = modality => predicate => s => s(modality)(predicate)
const get_modality = c => c((m, _) => m)
const get_predicate = c => c((_, p) => p)
const evaluate_constraint = constraint => pop =>
get_predicate(constraint)(pop)
const evaluate_with_modality = constraint => pop => {
const result = evaluate_constraint(constraint)(pop)
const modal = get_modality(constraint)
return pair(modal)(result)
}
const Violation = constraint => entity => reason => s => s(constraint)(entity)(reason)
// ───────────── Meta-Fact Declarations ─────────────
const entityType = name =>
FactSymbol("entityType")(cons(unit(name))(nil))
const factType = (verb, arity) =>
FactSymbol("factType")(cons(unit(verb))(cons(unit(arity))(nil)))
const role = (verb, index, name) =>
FactSymbol("role")(cons(unit(verb))(cons(unit(index))(cons(unit(name))(nil))))
const reading = (verb, parts) =>
FactSymbol("reading")(cons(unit(verb))(cons(parts)(nil)))
const inverseReading = (primary, inverse, order, template) =>
FactSymbol("inverseReading")(
cons(unit(primary))(
cons(unit(inverse))(
cons(order)(
cons(template)(nil))))
)
const constraint = (id, modality) =>
FactSymbol("constraint")(cons(unit(id))(cons(unit(modality))(nil)))
const constraintTarget = (constraintId, verb, roleIndex) =>
FactSymbol("constraintTarget")(cons(unit(constraintId))(cons(unit(verb))(cons(unit(roleIndex))(nil))))
const violation = (entity, constraintId, reason) =>
FactSymbol("violation")(cons(unit(entity))(cons(unit(constraintId))(cons(unit(reason))(nil))))
// ───────────── Reserved Symbols ─────────────
const RMAP = Symbol('RMAP')
const CSDP = Symbol('CSDP')
// ───────────── Exports ─────────────
module.exports = {
TRUE, FALSE, IF, AND, OR, NOT,
pair, fst, snd,
nil, cons, map, fold, append,
Entity, unit, bind, get_id,
equals, nth, mapIndex, reorder,
RelType, get_arity, get_verb, get_reading, get_constraints,
makeVerbFact, FactSymbol, get_verb_symbol, get_entities,
Reading: make_reading, get_reading_verb, get_reading_order, get_reading_template,
Event, emit_event, get_fact, get_time, get_event_readings,
unit_state, bind_state, make_transition, unguarded,
StateMachine, run_machine, run_entity,
Constraint, get_modality, get_predicate,
evaluate_constraint, evaluate_with_modality,
Violation,
entityType, factType, role, reading, inverseReading,
constraint, constraintTarget, violation,
ALETHIC, DEONTIC,
RMAP, CSDP
}
/*
* Lightweight Symbolic Forum Model Example
*
* Demonstrates:
* - Executable verbs
* - RelTypes and Readings
* - Inverse readings (manually declared)
* - Event emission with all readings
* - Deontic constraint requiring inverse reading
* - Minimal fact population with post/reply/moderation
*
// ───────────── Entities ─────────────
const alice = unit("alice")
const bob = unit("bob")
const thread1 = unit("thread-1")
const postA = unit("post-A")
const postB = unit("post-B")
// ───────────── RelType: posts ─────────────
const postsVerb = args => {
const [user, post, thread] = [nth(0)(args), nth(1)(args), nth(2)(args)]
return FactSymbol("posts")(args)
}
const postsType = RelType(3)(postsVerb)(
["", " posted ", " in ", ""]
)(nil)
// Reading: forward
reading("posts", ["", " posted ", " in ", ""])
// Inverse reading (manually declared)
inverseReading("posts", "receivesPostFrom", cons(2)(cons(1)(cons(0)(nil))),
["", " received ", " from ", ""])
// ───────────── RelType: replies ─────────────
const repliesVerb = args => {
const [user, replyPost, originalPost] = [nth(0)(args), nth(1)(args), nth(2)(args)]
return FactSymbol("replies")(args)
}
const repliesType = RelType(3)(repliesVerb)(
["", " replied with ", " to ", ""]
)(nil)
reading("replies", ["", " replied with ", " to ", ""])
// ───────────── RelType: moderates ─────────────
const moderatesVerb = args => {
const [moderator, post] = [nth(0)(args), nth(1)(args)]
return FactSymbol("moderates")(args)
}
const moderatesType = RelType(2)(moderatesVerb)(
["", " moderated ", ""]
)(nil)
reading("moderates", ["", " moderated ", ""])
// ───────────── Deontic Constraint: inverse required for posts ─────────────
const inverseRequiredForPosts = Constraint(DEONTIC)(
pop => {
const found = any(pop, f =>
get_verb_symbol(f) === "inverseReading" &&
get_id(nth(0)(get_entities(f))) === "posts"
)
return found ? TRUE : FALSE
}
)
constraint("inverse_required_for_posts", DEONTIC)
constraintTarget("inverse_required_for_posts", "posts", 0)
// ───────────── Fact Instances ─────────────
// Alice posts postA in thread1
const postFact = makeVerbFact(postsType)(alice)(postA)(thread1)
// Bob replies to postA with postB
const replyFact = makeVerbFact(repliesType)(bob)(postB)(postA)
// Alice moderates Bob's post
const modFact = makeVerbFact(moderatesType)(alice)(postB)
// ───────────── Events ─────────────
// Inverse reading list manually provided to emit_event
const inverseReadingsForPosts = cons(
make_reading("receivesPostFrom", cons(2)(cons(1)(cons(0)(nil))),
["", " received ", " from ", ""])
)(nil)
const event1 = emit_event(postFact, unit("t1"), inverseReadingsForPosts)
const event2 = emit_event(replyFact, unit("t2"))
const event3 = emit_event(modFact, unit("t3"))
// ───────────── Constraint Evaluation ─────────────
const pop = cons(
inverseReading("posts", "receivesPostFrom", cons(2)(cons(1)(cons(0)(nil))),
["", " received ", " from ", ""])
)(nil)
const evalResult = evaluate_with_modality(inverseRequiredForPosts)(pop)
// Expected: pair(DEONTIC)(TRUE)
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment