Cookbook
Logic
Logic programming with Eta’s built-in unification engine, constraint logic programming (CLP), and symbolic rewriting.
Run any example with:
etai cookbook/logic/<file>.eta
Unification Primitives
cookbook/logic/unification.eta · source
Eta provides six built-in unification primitives: logic-var, unify,
deref-lvar, logic-var?, trail-mark, and unwind-trail. Together they
give you the core machinery of a Prolog engine.
(module unification
(import std.io std.collections std.logic)
(begin
;; 1. Basic unification
(define x (logic-var))
(unify x 42)
(println (deref-lvar x)) ; => 42
(println (unify 'hello 'hello)) ; => #t
(println (unify 'hello 'world)) ; => #f
;; 2. Structural unification
;; Unify (f ?a 2) with (f 1 ?b)
(define a (logic-var))
(define b (logic-var))
(unify (cons 'f (cons a (cons 2 '())))
(cons 'f (cons 1 (cons b '()))))
(println (deref-lvar a)) ; => 1
(println (deref-lvar b)) ; => 2
;; Variable chains
(define v1 (logic-var))
(define v2 (logic-var))
(unify v1 v2) ; v1 and v2 share the same binding
(unify v2 99)
(println (deref-lvar v1)) ; => 99
;; 3. Backtracking with trail marks
(define r (logic-var))
(define mark (trail-mark))
(unify r 100)
(println (deref-lvar r)) ; => 100
(unwind-trail mark)
(println (logic-var? (deref-lvar r))) ; => #t (unbound again)
;; 4. Occurs check -- prevents cyclic terms
(define z (logic-var))
(define z-m (trail-mark))
(println (unify z (cons z '()))) ; => #f (would be infinite)
(unwind-trail z-m)
;; 5. Fact database queries
(define parent-db
'((tom bob) (tom liz) (bob ann) (bob pat) (pat jim)))
(defun solve-parent (pvar cvar db)
(if (null? db) '()
(let* ((fact (car db))
(m (trail-mark))
(ok (and (unify pvar (car fact))
(unify cvar (cadr fact)))))
(if ok
(let* ((sol (cons (deref-lvar pvar) (deref-lvar cvar)))
(rest (begin (unwind-trail m)
(solve-parent pvar cvar (cdr db)))))
(cons sol rest))
(begin (unwind-trail m)
(solve-parent pvar cvar (cdr db)))))))
;; Children of tom
(let* ((pv (logic-var)) (cv (logic-var))
(_ (unify pv 'tom))
(sols (solve-parent pv cv parent-db)))
(println (map* cdr sols))) ; => (bob liz)
;; 6. std.logic higher-level API
;; findall -- collect all solutions
(let* ((x (logic-var))
(sols (findall
(lambda () (deref-lvar x))
(map* (lambda (v) (lambda () (== x v)))
'(a b c)))))
(println sols)) ; => (a b c)
;; run1 -- first solution
(let* ((x (logic-var))
(sol (run1
(lambda () (deref-lvar x))
(map* (lambda (v) (lambda () (and (== x v) (> v 3))))
'(1 2 3 4 5)))))
(println sol)) ; => 4
;; succeeds? -- non-destructive probe
(let* ((sv (logic-var))
(ok (succeeds? (lambda () (== sv 77)))))
(println ok) ; => #t
(println (logic-var? sv))) ; => #t (binding not kept)
))
Relational Logic Programming
cookbook/logic/logic.eta · source
Wraps the fact database as a reusable relation function that returns a list of goal branches. The same relation works in all query directions, mirroring miniKanren / Prolog.
(module logic
(import std.io std.core std.collections std.logic)
(begin
(define parent-db
'((tom bob) (tom liz) (bob ann) (bob pat) (pat jim)))
;; parento -- the core relation.
;; Each branch is a thunk; findall unwinds the trail between them.
;; Either argument can be a free logic variable (wildcard) or a
;; concrete value (filter).
(defun parento (p c)
(map* (lambda (fact)
(lambda ()
(and (== p (car fact))
(== c (cadr fact)))))
parent-db))
;; Forward: children of tom
(let* ((cv (logic-var))
(sols (findall (lambda () (deref-lvar cv))
(parento 'tom cv))))
(println sols)) ; => (bob liz)
;; Backward: parents of ann
(let* ((pv (logic-var))
(sols (findall (lambda () (deref-lvar pv))
(parento pv 'ann))))
(println sols)) ; => (bob)
;; Unconstrained: all parent->child pairs
(let* ((pv (logic-var)) (cv (logic-var))
(sols (findall (lambda () (cons (deref-lvar pv) (deref-lvar cv)))
(parento pv cv))))
(println sols))
;; => ((tom . bob) (tom . liz) (bob . ann) (bob . pat) (pat . jim))
;; grandparento -- composing two relations
;; Prolog: grandparent(GP, GC) :- parent(GP, Mid), parent(Mid, GC).
(defun grandparento (gp gc)
(let* ((mid (logic-var))
(mids (findall (lambda () (deref-lvar mid))
(parento gp mid))))
(flatten
(map* (lambda (mid-val) (parento mid-val gc))
mids))))
;; Grandchildren of tom
(let* ((gc (logic-var))
(sols (findall (lambda () (deref-lvar gc))
(grandparento 'tom gc))))
(println sols)) ; => (ann pat)
))
N-Queens
cookbook/logic/nqueens.eta · source
The classic N-queens problem solved with CLP(FD) finite-domain constraints.
Each queen’s column is a logic variable with domain [1..N]; all-different
and diagonal constraints are posted; labelling uses first-fail strategy.
(module nqueens
(import std.core std.collections std.logic std.clp)
(begin
;; Build N fresh logic variables with domain [lo..hi]
(defun make-fd-vars (n lo hi)
(let loop ((i 0) (acc '()))
(if (>= i n) (reverse acc)
(let ((v (logic-var)))
(clp:domain v lo hi)
(loop (+ i 1) (cons v acc))))))
;; Post qi + offset = ai (fresh auxiliary variable)
(defun post-offsets (qs offs)
(let loop ((qs qs) (offs offs) (acc '()))
(cond
((null? qs) (reverse acc))
(else
(let ((a (logic-var)))
(clp:domain a -1000 1000)
(clp:plus-offset a (car qs) (car offs))
(loop (cdr qs) (cdr offs) (cons a acc)))))))
;; Solve N-queens, return column assignment or #f
(defun solve-nqueens (n)
(let* ((qs (make-fd-vars n 1 n))
(rows (let loop ((i 1) (acc '()))
(if (> i n) (reverse acc)
(loop (+ i 1) (cons i acc)))))
(nrows (map (lambda (i) (- 0 i)) rows))
(as (post-offsets qs rows)) ; Qi + i (anti-diagonal)
(bs (post-offsets qs nrows))) ; Qi - i (main diagonal)
(clp:all-different qs)
(clp:all-different as)
(clp:all-different bs)
(if (clp:labeling qs 'strategy 'ff)
(map (lambda (v) (deref-lvar v)) qs)
#f)))
;; Pretty-print board
(defun print-board (cols)
(let ((n (length cols)))
(for-each
(lambda (c)
(let loop ((i 1))
(cond
((> i n) (newline))
((= i c) (display "Q ") (loop (+ i 1)))
(else (display ". ") (loop (+ i 1))))))
cols)))
(let ((sol (solve-nqueens 8)))
(display "8-queens: ") (display sol) (newline)
(print-board sol))
))
SEND + MORE = MONEY
cookbook/logic/send-more-money.eta · source
The classic cryptarithmetic puzzle solved with CLP(FD). Each letter is a
distinct digit [0..9] with leading digits non-zero. A single
clp:scalar-product encodes the column-wise arithmetic constraint.
Unique solution: 9567 + 1085 = 10652.
(module send-more-money
(import std.core std.collections std.logic std.clp)
(begin
(defun solve ()
(let ((s (logic-var)) (e (logic-var)) (n (logic-var)) (d (logic-var))
(m (logic-var)) (o (logic-var)) (r (logic-var)) (y (logic-var)))
;; Leading digits non-zero
(clp:domain s 1 9)
(clp:domain m 1 9)
;; Remaining letters
(clp:domain e 0 9) (clp:domain n 0 9) (clp:domain d 0 9)
(clp:domain o 0 9) (clp:domain r 0 9) (clp:domain y 0 9)
;; All eight digits distinct
(clp:all-different (list s e n d m o r y))
;; SEND + MORE - MONEY = 0 linearised:
;; S:+1000 E:+91 N:-90 D:+1 M:-9000 O:-900 R:+10 Y:-1
(clp:scalar-product
'(1000 91 -90 1 -9000 -900 10 -1)
(list s e n d m o r y)
0)
(if (clp:labeling (list s e n d m o r y) 'strategy 'ff)
(list (cons 'S (deref-lvar s)) (cons 'E (deref-lvar e))
(cons 'N (deref-lvar n)) (cons 'D (deref-lvar d))
(cons 'M (deref-lvar m)) (cons 'O (deref-lvar o))
(cons 'R (deref-lvar r)) (cons 'Y (deref-lvar y)))
#f)))
(let ((sol (solve)))
(for-each
(lambda (kv)
(display (car kv)) (display " = ") (display (cdr kv)) (newline))
sol))
;; S = 9, E = 5, N = 6, D = 7, M = 1, O = 0, R = 8, Y = 2
))
Boolean Simplifier
cookbook/logic/boolean-simplifier.eta · source
Symbolic Boolean expression simplifier using recursive tree rewriting. Applies De Morgan’s laws, double-negation elimination, identity laws, and idempotency. A fixed-point wrapper runs until the expression stabilises.
(module boolean-simplifier
(import std.io)
(begin
(defun non-pair? (x) (not (pair? x)))
(defun op (e) (car e))
(defun a1 (e) (car (cdr e)))
(defun a2 (e) (car (cdr (cdr e))))
(defun simplify-bool (e)
(cond
((non-pair? e) e)
;; not
((eq? (op e) 'not)
(let ((u (simplify-bool (a1 e))))
(cond
((eq? u #t) #f)
((eq? u #f) #t)
;; Double negation: (not (not x)) = x
((and (pair? u) (eq? (op u) 'not)) (a1 u))
;; De Morgan: (not (and a b)) = (or (not a) (not b))
((and (pair? u) (eq? (op u) 'and))
(list 'or (list 'not (a1 u)) (list 'not (a2 u))))
;; De Morgan: (not (or a b)) = (and (not a) (not b))
((and (pair? u) (eq? (op u) 'or))
(list 'and (list 'not (a1 u)) (list 'not (a2 u))))
(#t (list 'not u)))))
;; and
((eq? (op e) 'and)
(let ((sa (simplify-bool (a1 e)))
(sb (simplify-bool (a2 e))))
(cond
((eq? sa #f) #f) ; x AND F = F
((eq? sb #f) #f) ; F AND x = F
((eq? sa #t) sb) ; x AND T = x
((eq? sb #t) sa) ; T AND x = x
((equal? sa sb) sa) ; x AND x = x
(#t (list 'and sa sb)))))
;; or
((eq? (op e) 'or)
(let ((sa (simplify-bool (a1 e)))
(sb (simplify-bool (a2 e))))
(cond
((eq? sa #t) #t) ; x OR T = T
((eq? sb #t) #t) ; T OR x = T
((eq? sa #f) sb) ; x OR F = x
((eq? sb #f) sa) ; F OR x = x
((equal? sa sb) sa) ; x OR x = x
(#t (list 'or sa sb)))))
(#t e)))
;; Fixed-point: keep simplifying until stable
(defun simplify-bool* (e)
(let ((s (simplify-bool e)))
(if (equal? s e) s (simplify-bool* s))))
;; Examples
(println (simplify-bool* '(and x #t))) ; => x
(println (simplify-bool* '(and x #f))) ; => #f
(println (simplify-bool* '(or (and #t x) #f))) ; => x
(println (simplify-bool* '(not (not y)))) ; => y
(println (simplify-bool* '(not (and a b)))) ; => (or (not a) (not b))
(println (simplify-bool* '(not (or a b)))) ; => (and (not a) (not b))
(println (simplify-bool* '(or x x))) ; => x
))
Symbolic Differentiation
cookbook/logic/symbolic-diff.eta · source
Symbolic differentiation and algebraic simplification of expression trees.
Supports: constants, variables, +, *, ^ (power), sin, cos, exp,
log. A fixed-point simplifier cleans up the result.
(module symbolic-diff
(import std.io)
(begin
(defun non-pair? (x) (not (pair? x)))
(defun op (e) (car e))
(defun a1 (e) (car (cdr e)))
(defun a2 (e) (car (cdr (cdr e))))
;; Algebraic simplifier
(defun simplify (e)
(cond
((non-pair? e) e)
((eq? (op e) '+)
(let ((sa (simplify (a1 e))) (sb (simplify (a2 e))))
(cond
((and (number? sa) (= sa 0)) sb)
((and (number? sb) (= sb 0)) sa)
((and (number? sa) (number? sb)) (+ sa sb))
((equal? sa sb) (list '* 2 sa))
(#t (list '+ sa sb)))))
((eq? (op e) '*)
(let ((sa (simplify (a1 e))) (sb (simplify (a2 e))))
(cond
((or (and (number? sa) (= sa 0))
(and (number? sb) (= sb 0))) 0)
((and (number? sa) (= sa 1)) sb)
((and (number? sb) (= sb 1)) sa)
((and (number? sa) (number? sb)) (* sa sb))
(#t (list '* sa sb)))))
(#t e)))
(defun simplify* (e)
(let ((s (simplify e)))
(if (equal? s e) s (simplify* s))))
;; Symbolic differentiation rules
(defun diff (e v)
(cond
((number? e) 0)
((symbol? e) (if (eq? e v) 1 0))
;; Sum rule: d(a+b) = da + db
((eq? (op e) '+)
(list '+ (diff (a1 e) v) (diff (a2 e) v)))
;; Product rule: d(a*b) = da*b + a*db
((eq? (op e) '*)
(list '+ (list '* (diff (a1 e) v) (a2 e))
(list '* (a1 e) (diff (a2 e) v))))
;; Power rule: d(u^n) = n*u^(n-1)*du
((eq? (op e) '^)
(let ((u (a1 e)) (n (a2 e)))
(if (number? n)
(list '* n (list '* (list '^ u (- n 1)) (diff u v)))
0)))
;; Chain rules
((eq? (op e) 'sin)
(list '* (list 'cos (a1 e)) (diff (a1 e) v)))
((eq? (op e) 'cos)
(list '* -1 (list '* (list 'sin (a1 e)) (diff (a1 e) v))))
((eq? (op e) 'exp)
(list '* (list 'exp (a1 e)) (diff (a1 e) v)))
((eq? (op e) 'log)
(list '* (list '/ 1 (a1 e)) (diff (a1 e) v)))
(#t 0)))
;; Differentiate and simplify
(defun D (expr var) (simplify* (diff expr var)))
;; Examples
(println (D '(+ (* x x) 3) 'x)) ; => (* 2 x)
(println (D '(* x (+ x 3)) 'x)) ; => (+ (+ x 3) x)
(println (D '(sin (* x x)) 'x)) ; => (* (cos (* x x)) (* 2 x))
(println (D '(^ (+ x 1) 3) 'x)) ; => (* 3 (^ (+ x 1) 2))
(println (D '(exp (* 2 x)) 'x)) ; => (* (exp (* 2 x)) 2)
(println (D '(log x) 'x)) ; => (/ 1 x)
))