Skip to content

Instantly share code, notes, and snippets.

@lantiga
Last active August 29, 2015 14:10
Show Gist options
  • Save lantiga/b65dd7a1d7c3bb35a7b1 to your computer and use it in GitHub Desktop.
Save lantiga/b65dd7a1d7c3bb35a7b1 to your computer and use it in GitHub Desktop.
Naive pure rule engine
(ns carla.core
(:require [clojure.math.combinatorics :as combo]))
(defn- fact-matches? [fact match]
(= match (select-keys fact (keys match))))
(defn make-rules [] [])
(defn make-session [] {:facts #{}})
(defn add-rule [rules match-map & conds-and-body]
(let [conds (butlast conds-and-body)
body (last conds-and-body)
match-keys (keys match-map)
match-vals (vals match-map)
rule (fn [session]
(->> match-vals
(map (fn [match] (filter #(fact-matches? % match) (:facts session))))
(apply combo/cartesian-product)
(map #(zipmap match-keys %))
(filter (fn [matched] (every? (fn [cnd] (cnd session matched)) conds)))
(reduce body session)))]
(conj rules rule)))
(defn insert-fact [session fact]
(update-in session [:facts] conj fact))
(defn retract-fact [session fact]
(update-in session [:facts] disj fact))
(defn match-facts [session match]
(filter #(fact-matches? % match) (:facts session)))
(defn fire-rules [session rules]
(reduce (fn [s rule] (rule s)) session rules))
(defn fire-rules* [session rules max-iter]
(loop [session session
c 0]
(let [new-session (fire-rules session rules)]
(if (or (= new-session session) (= c max-iter))
session
(recur new-session (inc c))))))
(defn doit []
(let [rules (-> (make-rules)
(add-rule
{:tick {:type :Tick}
:init {:type :Initialized}}
(fn [s {{stamp :stamp} :tick {timestamp :timestamp} :init}]
(= stamp timestamp))
(fn [s {{tick-boo :boo} :tick {init-boo :boo} :init}]
(and tick-boo init-boo))
(fn [s {{stamp :stamp boo :boo} :tick}]
(insert-fact s {:type :Tock :stamp stamp :boo boo}))))]
(->
(make-session)
(insert-fact {:type :Tick :stamp 100 :boo false})
(insert-fact {:type :Tick :stamp 100 :boo true})
(insert-fact {:type :Tick :stamp 200 :boo true})
(insert-fact {:type :Initialized :timestamp 100 :boo true})
(insert-fact {:type :Initialized :timestamp 300 :boo false})
(fire-rules* rules 10)
(match-facts {:type :Tock}))))
(defproject carla "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/math.combinatorics "0.0.8"]])
@lantiga
Copy link
Author

lantiga commented Nov 20, 2014

Accumulators (i.e. pre-filtering or accumulating individual matches before conditions are fired) could be added as

(add-rule
  {:tick [{:type :Tick} (fn [matches] 
                                 (let [max-stamp (max (map :stamp matches))]
                                   (filter #(= (:stamp %) max-stamp) matches)))]
  ...

@giorgio-v
Copy link

I’m surely missing something: why do you need to reduce over session twice? Once in fire-rules and then in the rule body?

@giorgio-v
Copy link

Also, I seem to remember that reduce is not lazy (in 1.6 at least)? OK, this is a naive rule engine but if you plan to build upon this experiment it could worth considering the implications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment