A library providing an alternative syntax for Clara Rules. After deep contemplation, thorough discussion with the greatest minds I know, and summoning the willpower of a Greek god, I decided to resist calling it O'Doyle Rules. You are welcome.
Why does clara need a new coat of paint? Consider an example using clara's current syntax:
(ns examples.clara
(:require [clara.rules :as clara #?(:clj :refer :cljs :refer-macros) [defsession defquery defrule]]
[clara.rules.accumulators :as acc]))
(defrecord Player [x y health])
(defrecord Enemy [x y health])
(defrule remove-dead-enemies
[?enemy <- Enemy (= health 0)]
=>
(clara/retract! ?enemy))
(defquery get-player
[]
[?player <- Player])
(defquery get-nearby-enemies
[]
[?player <- Player]
[?enemy <- (acc/all) :from [Enemy (= (:x ?player) x) (= (:y ?player) y)]])
(defquery get-enemies-at
[:?x :?y]
[?enemy <- (acc/all) :from [Enemy (= ?x x) (= ?y y)]])
(defsession session 'examples.clara)
(def *session
(-> session
(clara/insert (->Player 1 1 10)
(->Enemy 1 1 10)
(->Enemy 1 1 10)
(->Enemy 2 2 10)
(->Enemy 2 2 0))
clara/fire-rules
atom))
(-> (clara/query @*session get-player)
first :?player println)
;; => #examples.clara.Player{:x 1, :y 1, :health 10}
(-> (clara/query @*session get-nearby-enemies)
first :?enemy println)
;; => [#examples.clara.Enemy{:x 1, :y 1, :health 10} #examples.clara.Enemy{:x 1, :y 1, :health 10}]
(-> (clara/query @*session get-enemies-at :?x 2 :?y 2)
first :?enemy println)
;; => [#examples.clara.Enemy{:x 2, :y 2, :health 10}]
;; this only returns one, because the other enemy at (2, 2) was removed by the remove-dead-enemies rule
I see two main problems:
<-
thingy for binding local names, the :from [...]
syntax for accumulators, and so on. I think we can make this more intuitive.def
it themselves if they want to.Here's the same program using clarax:
(ns examples.clarax
(:require [clara.rules :as clara]
[clara.rules.accumulators :as acc]
#?(:clj [clarax.macros-java :refer [->session]]
:cljs [clarax.macros-js :refer-macros [->session]])))
(defrecord Player [x y health])
(defrecord Enemy [x y health])
(def *session
(-> ;; all rules/queries are specified in a single hash map.
;; as you can see, rules look like familiar `let` expressions,
;; mapping a name (enemy) to a record type (Enemy).
;; any binding pair can be followed by a :when expression,
;; similar to the :when expressions in `for`, `doseq`, etc.
{:remove-dead-enemies
(let [enemy Enemy
:when (= (:health enemy) 0)]
;; the body of the `let` is what will run when the rule fires
(clara/retract! enemy))
;; queries look exactly like rules, except the are surrounded by `fn`
:get-player
(fn []
(let [player Player]
;; the body of the `let` determines what the query will return.
;; in this case, it will simply return the entire player record.
player))
:get-nearby-enemies
(fn []
(let [player Player
enemy Enemy
:accumulator (acc/all) ;; this is how you use an accumulator
:when (and (= (:x player) (:x enemy))
(= (:y player) (:y enemy)))]
enemy))
:get-enemies-at
(fn [?x ?y]
(let [{:keys [x y] :as enemy} Enemy ;; you can destructure just like in a normal `let` form
:accumulator (acc/all)
:when (and (= ?x x) (= ?y y))]
enemy))}
;; this macro creates the session from the hash map
->session
;; you use the same functions from clara to insert and fire rules
(clara/insert (->Player 1 1 10)
(->Enemy 1 1 10)
(->Enemy 1 1 10)
(->Enemy 2 2 10)
(->Enemy 2 2 0))
clara/fire-rules
atom))
(println (clara/query @*session :get-player))
;; => #examples.clara.Player{:x 1, :y 1, :health 10}
(println (clara/query @*session :get-nearby-enemies))
;; => [#examples.clara.Enemy{:x 1, :y 1, :health 10} #examples.clara.Enemy{:x 1, :y 1, :health 10}]
(println (clara/query @*session :get-enemies-at :?x 2 :?y 2))
;; => [#examples.clara.Enemy{:x 2, :y 2, :health 10}]
;; this only returns one, because the other enemy at (2, 2) was removed by the remove-dead-enemies rule
It's possible for that map of rules and queries to become too large, at which point you'll get a Method code too large!
error. And since ->session
is a macro, it needs that map to exist at compile time, so merging smaller maps together at runtime won't work. Instead, you can merge the maps together at compile time with a macro. See the dungeon-crawler game for an example of this. This works for ClojureScript as well.
clj -A:dev
clj -A:test
clj -A:prod install
All files that originate from this project are dedicated to the public domain. I would love pull requests, and will assume that they are also dedicated to the public domain.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close