Skip to content

Instantly share code, notes, and snippets.

@sam-lippert
Last active April 9, 2025 12:15
Show Gist options
  • Save sam-lippert/d0567948ab11b050e34f13b3459b7897 to your computer and use it in GitHub Desktop.
Save sam-lippert/d0567948ab11b050e34f13b3459b7897 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

Then, in your code:

const {
  TRUE, FALSE, IF, AND, OR, NOT,
  pair, fst, snd,
  nil, cons, map, fold, append,
  Entity, unit, bind, get_id,
  equals, nth,
  RelType, get_arity, get_verb, get_reading, get_constraints,
  makeVerbFact, FactSymbol, get_verb_symbol, get_entities,
  Event, get_fact, get_time,
  unit_state, bind_state,
  make_transition, unguarded,
  StateMachine, run_machine, run_entity,
  Constraint, get_modality, get_predicate,
  evaluate_constraint, evaluate_with_modality,
  Violation,
  mandatory_role_constraint,
  entityType, factType, role, reading,
  constraint, constraintTarget, violation,
  ALETHIC, DEONTIC,
  RMAP, CSDP
} = require('exec-symbols')

Quick Start

  1. Create a RelType (relationship type) with a specified arity (the number of entity arguments).
  2. Use makeVerbFact to build a curried function that expects that many entities.
  3. Represent facts using FactSymbol (if you just need a symbolic representation).
  4. Model constraints as needed, and evaluate them against a collection of facts (the “population”).
  5. Use Event and StateMachine to process a stream of events that update your system state.

Core Concepts

Church Booleans

const TRUE  = t => f => t
const FALSE = t => f => f
const IF    = b => t => e => b(t)(e)

// AND, OR, NOT
const AND   = p => q => p(q)(FALSE)
const OR    = p => q => p(TRUE)(q)
const NOT   = p => p(FALSE)(TRUE)
  • These are Church-encoded booleans. They are functions that, given two branches, choose one to evaluate.

Church Lists and Pairs

// Pairs
const pair = a => b => f => f(a)(b)
const fst  = p => p((a, _) => a)
const snd  = p => p((_, b) => b)

// Lists
const nil  = c => n => n
const cons = h => t => c => n => c(h)(t(c)(n))
const fold = f => acc => l => l(f)(acc)
const map  = f => l => ...
const append = l1 => l2 => ...
  • A pair is stored as a function that takes a function f and applies f(a)(b).
  • A list is stored as a function that takes a function for the "cons" case (c) and a function for the "nil" case (n).

Entities and Binding

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)

  • An Entity is also a function (the same Church-style approach).
  • unit creates an entity from an identifier.
  • bind gives a way to compose entity transformations (similar to a monad).

Relationships and Facts

Relationship Types

const RelType = arity => verbFn => reading => constraints =>
  s => s(arity)(verbFn)(reading)(constraints)
  • A RelType captures:
    1. Arity (number of entities in the relationship),
    2. A verb function,
    3. A reading (a textual representation or something similar),
    4. Constraints (additional rules).

makeVerbFact:

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)
}
  • Takes a RelType and returns a curried function that expects exactly arity number of entities. Once all entities are provided, it executes the underlying verb function.

Symbolic Facts

const FactSymbol = verb => entities => s => s(verb)(entities)
  • A quick way to represent a fact as (verb, [entities]) in a Church-encoded closure.

Events

const Event = fact => time => s => s(fact)(time)
const get_fact = e => e((f, t) => f)
const get_time = e => e((f, t) => t)
  • An Event pairs a fact with a time, again using a function-based approach.

State Machines

// State Monad
const unit_state = a => s => pair(a)(s)
const bind_state = m => f => s => { /* typical state-monad logic */ }

// Transition
const make_transition = guard => compute_next =>
  state => input =>
    IF(guard(state)(input))(
      compute_next(state)(input)
    )(
      state
    )

// StateMachine
const StateMachine = transition => initial => s => s(transition)(initial)

// Running a machine
const run_machine = machine => stream =>
  machine((transition, initial) =>
    fold(event => state =>
      transition(state)(get_fact(event))
    )(initial)(stream)
  )
  • The code includes guarded transitions (using Church booleans) and a state monad for carrying and updating state.
  • StateMachine encapsulates a transition function and an initial state.
  • run_machine processes a stream of events (Church-encoded list) against the transition function.

Constraints and Violations

const Constraint = modality => predicate => s => s(modality)(predicate)
const Violation  = constraint => entity => reason => s => s(constraint)(entity)(reason)
  • Constraints contain:

    • Modality (e.g., ALETHIC or DEONTIC).
    • A predicate function to evaluate over a "population."
  • A Violation is a record of which entity violated which constraint, and why.

Example mandatory_role_constraint:

const mandatory_role_constraint = (roleIndex, relType) => pop => {
  // Ensures that every entity designated for that role
  // actually appears in a fact with the matching verb
}
  • Verifies that all entities that occupy a certain "role" in a relationship appear in the "population" of facts.

Examples

Simple Boolean Usage

const isTrue = IF(TRUE)('yes')('no') // 'yes'
const isFalse = IF(FALSE)('yes')('no') // 'no'

Building a Relationship and a Fact

// Define a relationship type: "loves", arity = 2
const lovesRelType = RelType(2)(
  args => {
    // A simple verb function that returns a FactSymbol
    return FactSymbol('loves')(args)
  }
)(
  "X loves Y"        // reading
)(
  nil                // no additional constraints
)

// Make a verb fact for "loves"
const loves = makeVerbFact(lovesRelType)

// Provide two entities
const alice = unit("Alice")
const bob   = unit("Bob")

// Curried usage
const fact = loves(alice)(bob) // => FactSymbol('loves')(cons(alice)(cons(bob)(nil)))

// Inspect
console.log(get_verb_symbol(fact))     // 'loves'
console.log(get_id(nth(0)(get_entities(fact)))) // 'Alice'
console.log(get_id(nth(1)(get_entities(fact)))) // 'Bob'

Basic State Machine Example

// Define a simple guard and next state
const guard = state => input => 
  // For demonstration, only proceed if input matches "go"
  equals(input)(unit("go"))

const compute_next = state => input => 
  // Return a new state, e.g., "running"
  pair(unit("running"))(snd(state))

// Make a transition
const transition = make_transition(guard)(compute_next)

// Initial machine
const myMachine = StateMachine(transition)(pair(unit("idle"))(nil))

// Stream of events
const eventStream = cons(Event(unit("go"))(0))(
                    cons(Event(unit("stop"))(1))(nil))

// Run
const finalState = run_machine(myMachine)(eventStream)
console.log(get_id(fst(finalState))) // "running" if it processed "go"

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 Event
const readingsForPosts = cons(
  make_reading("receivesPostFrom", cons(2)(cons(1)(cons(0)(nil))),
    ["", " received ", " from ", ""])
)(nil)

const event1 = Event(postFact, unit("t1"), readingsForPosts)
const event2 = Event(replyFact, unit("t2"))
const event3 = 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)

License

This library is provided as-is for learning, experimentation, and reference. Feel free to adapt it for your own purposes. MIT License

// #region Lambda Calculus Primitives
const IDENTITY = (n) => n
const TRUE = (trueCase) => (falseCase) => trueCase
const FALSE = (trueCase) => (falseCase) => falseCase
const IF = (condition) => (trueCase) => (falseCase) => condition(trueCase)(falseCase)
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 = (pair) => pair(TRUE)
const snd = (pair) => pair(FALSE)
const nil = (c) => TRUE
const ISEMPTY = (L) => L((head) => (tail) => FALSE)
const cons = (head) => (tail) => (selector) => selector(head)(tail)
const Θ = (le) => ((x) => le((y) => x(x)(y)))((x) => le((y) => x(x)(y)))
const fold = Θ(
(recFold) => (f) => (acc) => (list) =>
IF(ISEMPTY(list))(acc)(list((head) => (tail) => f(head)(recFold(f)(acc)(tail)))),
)
const map = (f) => (list) => fold((x) => (acc) => cons(f(x))(acc))(nil)(list)
const append = (l1) => (l2) => fold(cons)(l2)(l1)
// #endregion
// #region Arithmatic
const ZERO = (a) => (b) => b
const SUCC = (n) => (a) => (b) => a(n(a)(b))
const ADD = (m) => (n) => (a) => (b) => m(SUCC)(n)(a)(b)
const MULT = (m) => (n) => (a) => (b) => m(n(a))(b)
const EXP = (m) => (n) => (a) => (b) => n(m)(a)(b)
const PRED = (n) => (f) => (x) => n((g) => (h) => h(g(f)))((u) => x)((u) => u)
const SUB = (m) => (n) => n(PRED)(m)
const ISZERO = (n) => n((x) => ZERO)(TRUE)
const EQ = (m) => (n) => AND(LE(m)(n))(LE(n)(m))
const LE = (m) => (n) => ISZERO(SUB(m)(n))
const GE = (m) => (n) => ISZERO(SUB(n)(m))
const LT = (m) => (n) => NOT(GE(m)(n))
const GT = (m) => (n) => NOT(LE(m)(n))
// #endregion
// #region Utilities
const equals = (a) => (b) => get_id(a) === get_id(b)
const nth = (n) => (list) =>
Θ(
(recNth) => (targetN) => (currentList) => (currentIndex) =>
IF(ISEMPTY(currentList))(nil)(
IF(EQ(currentIndex)(targetN))(currentList((h) => (_) => h))(
currentList((h) => (t) => recNth(targetN)(t)(SUCC(currentIndex))),
),
),
)(n)(list)(ZERO)
const mapIndex = (list) =>
Θ(
(recMapIndex) => (currentList) => (currentIndex) =>
IF(ISEMPTY(currentList))(nil)(
currentList((h) => (t) => cons(unit(currentIndex))(recMapIndex(t)(SUCC(currentIndex))))(nil),
),
)(list)(ZERO)
const reorder = (entities, order) => map((i) => nth(i)(entities))(order)
// #endregion
// #region 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)
// #endregion
// #region 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)
// #endregion
// #region FactType
const FactType = (arity) => (verbFn) => (reading) => (constraints) => (s) => s(arity, verbFn, reading, constraints)
// These getter functions need to work with the new selector pattern
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)
// #endregion
// #region Executable Facts
const makeVerbFact = (FactType) => {
const arity = get_arity(FactType)
const verb = get_verb(FactType)
// When arity is 0, just return the verb with empty list
// Otherwise, return a curried function to collect all arguments
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)
// #endregion
// #region 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)
// #endregion
// #region 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)
// #endregion
// #region 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)
// #endregion
// #region 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))))
// #endregion
// #region Reserved Symbols
const CSDP = Symbol('CSDP')
const RMAP = Symbol('RMAP')
// #endregion
export default {
/**
* The identity function.
* @param n - The value to return.
* @returns The value.
*/
IDENTITY,
/**
* The true function.
* Returns the first parameter.
* @param trueCase - The value to return if the condition is true.
* @param falseCase - The value to return if the condition is false.
* @returns The value.
*/
TRUE,
/**
* The false function.
* Returns the second parameter. (equivalent ZERO)
* @param trueCase - The value to return if the condition is true.
* @param falseCase - The value to return if the condition is false.
* @returns The value.
*/
FALSE,
/**
* The if function.
* Returns the first parameter if the condition is true, otherwise the second parameter.
* @param condition - The condition to check.
* @param trueCase - The value to return if the condition is true.
* @param falseCase - The value to return if the condition is false.
* @returns The value.
*/
IF,
/**
* The and function.
* Returns true if both parameters are true.
* @param p - The first value.
* @param q - The second value.
* @returns The value.
*/
AND,
/**
* The or function.
* Returns true if either parameter is true.
* @param p - The first value.
* @param q - The second value.
* @returns The value.
*/
OR,
/**
* The not function.
* Negates the parameter.
* @param p - The value to negate.
* @returns The value.
*/
NOT,
/**
* The pair function.
* Returns a pair of the two parameters.
* @param a - The first value.
* @param b - The second value.
* @returns The value.
*/
pair,
/**
* Returns the first value of the pair.
* @param p - The pair.
* @returns The value.
*/
fst,
/**
* Returns the second value of the pair.
* @param p - The pair.
* @returns The value.
*/
snd,
nil,
ISEMPTY,
cons,
map,
fold,
append,
/**
* The zero function.
* Returns the identity function as a constant. (equivalent to FALSE)
* @param a - The function to apply to the pair.
* @returns The value.
*/
ZERO,
/**
* The successor function.
* Returns the successor of the parameter.
* @param n - The value of which to get the successor.
* @returns The value.
*/
SUCC,
/**
* The addition function.
* Returns the sum of the two parameters.
* @param m - The first value.
* @param n - The second value.
* @returns The value.
*/
ADD,
/**
* The multiplication function.
* Returns the product of the two parameters.
* @param m - The first value.
* @param n - The second value.
* @returns The value.
*/
MULT,
/**
* The exponentiation function.
* Returns the first parameter raised to the power of the second parameter.
* @param m - The base.
* @param n - The exponent.
* @returns The value.
*/
EXP,
/**
* The equality function.
* Returns true if the two parameters are equal.
* @param a - The first value.
* @param b - The second value.
* @returns The value.
*/
EQ,
/**
* The less than function.
* Returns true if the first parameter is less than the second parameter.
* @param m - The first value.
* @param n - The second value.
* @returns The value.
*/
LT,
/**
* The greater than function.
* Returns true if the first parameter is greater than the second parameter.
* @param m - The first value.
* @param n - The second value.
* @returns The value.
*/
GT,
/**
* The less than or equal to function.
* Returns true if the first parameter is less than or equal to the second parameter.
* @param m - The first value.
* @param n - The second value.
* @returns The value.
*/
LE,
/**
* The greater than or equal to function.
* Returns true if the first parameter is greater than or equal to the second parameter.
* @param m - The first value.
* @param n - The second value.
* @returns The value.
*/
GE,
Entity,
unit,
bind,
get_id,
/**
* The equals function.
* Returns true if two references are to the same object.
* @param a - The first value.
* @param b - The second value.
* @returns The value.
*/
equals,
/**
* The nth function.
* Returns the nth element of the list.
* @param n - The index of the element to return.
* @param list - The list to search.
*/
nth,
mapIndex,
reorder,
FactType,
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,
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,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment