clojure/script rule engine
beta quality
;; in deps.edn
{:deps {github-akovantsev/rules
{:git/url "https://github.com/akovantsev/rules"
:sha "87108b4fa2a0caceea1a59c284ac3baf38108819"}}} ;; actual sha
https://github.com/akovantsev/rules/blob/main/test/com/akovantsev/rules/test.clj#L56
https://gist.github.com/akovantsev/61476fe57dd8d11c1a492a1a1e82c31a
(def autoinc
(r/rule ::foo ;; macro, rule-id
[:new id :x x < 5] ;; find all ids whos :x is < 5
[:let id :y y] ;; join on id, where :y is not nil
[:old id :x ox not= x] ;; join on id, where prev :x val โ curr :x val
:foreach ;; for each match {:id ... :x ... :y ...}:
(prn :foreach1 match) ;; print match map
(r/change! id :x inc) ;; ~ (update current-db id :x inc)
:forall ;; once, for matches seq ({:id ... :x ... :y ...} ...):
(println :once1 matches))) ;; print matches
Similar to https://github.com/oakes/odoyle-rules with some differences:
odoyle
implementsrete
.rules
does not.odoyle
discards datoms wich don't have matching tuples in any rules.rules
does not, so you can store more things than you query, and don't keep anotherdb
map on the side.odoyle
allows to compare only(?) value bindings with only their own past value.rules
allows you to compare with any past value:
; odoyle:
:what
[foo ::left-of bar {:then not=}]
[bar ::color color]
; rules:
[:new bar ::color color my-compare-with obar]
[:new foo ::left-of bar not= obar]
[:old foo ::left-of obar]
Tuples' order within rule does not matter.
Tuples are rearranged during rule parsing.
[tuple-type e a v]
[tuple-type e a v f & args]
tuple-type
is one of:
:new
will usecurrent db
for bindings; any change in db'sa
(ttribute) willtrigger
rule.:old
will useprevious db
for bindings; does nottrigger
rule.:let
will usecurrent db
for bindnigs; does nottrigger
rule.
; e.g when db changes from {1 {:a 1 :b 1}} to {1 {:a 2 :b 2}}
; change of any :a attribute in db does not trigger this rule,
; because it is not used in :new.
; only changes of :b in db trigger rule:
[:old id :a oa] ;; :old uses :a, but :a change in db does not trigger rule
[:let id :a a not= (+ 1 oa)] ;; :let uses :a, but :a change in db does not trigger rule
[:new id :b b] ;; :new uses :b, and any :b change in db triggers the rule
triggers
- means rule is queued for query/LHS
, and if there are matches - forall/foreach/then/then-finally/RHS/RightHandSide
part of the rule is executed:
- first
foreach
(if present) is executed with each match fromquery
part (e.g. 10 patches => 10foreach
calls) - then
forall
(if present) is executed once with all the matches (e.g. 100 matches => 1forall
call)
Guards are (additional to joins) constraints on v
bindings (not e
ones).
You can reference any other binding from rule in predicate.
By default e
binding is passed as first arg to f
:
;;[type e a v f & args]:
[:new id :a a < b] ;; -> (fn [{:as match, :syms [a b]}] (< a b))
[:new id :b b odd?]
But you can specify place with %
.
[:new id :a a < b % (+ 10 id %)] ;; -> (fn [{:as match, :syms [a b id]}] (< b a (+ 10 id a))
[:new id :b b]
There are 2 rule callbacks available.
You can use both in the same rule.
Both are optional. Rule without any of those means it is just a query
.
Order of appearance in rule definition does not matter.
:foreach
is called for every match of query/LHS
, and sees:
- current
db
(same assession
inodoyle
andclara
), - all
matches
seq, - current
match
map, - all
e
andv
bindings,
Forms after:foreach
until the:forall
or end of rule are the body of callback:
...
[:new id :a a]
:foreach
(println db matches match)
(println id a)
;; => (fn [db matches {:as match, :syms [id a]}]
;; (println db matches match)
;; (println id a))
:forall
is is called only once, after :foreach
(if there is :foreach
in the rule), and sees only:
- current
db
, - all
matches
seq.
:forall
(println db)
(println matches)
;; => (fn [db matches]
;; (println db)
;; (println matches))