Implement rules!

This commit is contained in:
Jeremy Dormitzer 2019-07-19 18:54:49 -04:00
parent 9f501dcd09
commit 04ce4421d0
6 changed files with 113 additions and 74 deletions

View File

@ -1,12 +1,9 @@
(ns sicp-logic.assertions (ns sicp-logic.assertions
(:require [sicp-logic.db :as db] (:require [sicp-logic.db :refer [fetch-assertions]]
[sicp-logic.match :refer [pattern-match]])) [sicp-logic.match :refer [unify-match]]))
(defn fetch-assertions [db query frame]
(db/fetch-assertions db query frame))
(defn check-an-assertion [assertion query frame] (defn check-an-assertion [assertion query frame]
(let [match-result (pattern-match query assertion frame)] (let [match-result (unify-match query assertion frame)]
(if (= match-result :failed) (if (= match-result :failed)
[] []
[match-result]))) [match-result])))

View File

@ -1,6 +1,6 @@
(ns sicp-logic.core (ns sicp-logic.core
(:require [sicp-logic.binding :refer [instantiate]] (:require [sicp-logic.binding :refer [instantiate]]
[sicp-logic.db :refer [add-assertion]] [sicp-logic.db :refer [add-assertion add-rule]]
[sicp-logic.evaluator :refer [qeval]])) [sicp-logic.evaluator :refer [qeval]]))
(defn contract-question-mark [v] (defn contract-question-mark [v]
@ -27,15 +27,19 @@
(defmacro query [db q] (defmacro query [db q]
"Queries the database for assertions that match the query." "Queries the database for assertions that match the query."
`(map (fn [frame#] (let [processed-q (query-syntax-process q)]
(instantiate (query-syntax-process (quote ~q)) `(map (fn [frame#]
frame# (instantiate (quote ~processed-q)
(fn [v# f#] (contract-question-mark v#)))) frame#
(qeval ~db (query-syntax-process (quote ~q)) [{}]))) (fn [v# f#] (contract-question-mark v#))))
(qeval ~db (quote ~processed-q) [{}]))))
(defn assert! [db assertion] (defn assert! [db assertion]
"Adds a new assertion to the database." "Adds a new assertion to the database."
(add-assertion db assertion)) (add-assertion db assertion))
(defmacro defrule [] (defmacro defrule [db conclusion body]
"Adds a new rule to the database.") "Adds a new rule to the database."
(let [processed-conclusion (query-syntax-process conclusion)
processed-body (query-syntax-process body)]
`(add-rule ~db (quote [~processed-conclusion ~processed-body]))))

View File

@ -9,5 +9,5 @@
"Stores an assertion (a fact) in the database.") "Stores an assertion (a fact) in the database.")
(fetch-rules [db query frame] (fetch-rules [db query frame]
"Fetches rules whose conditions may unify with the given query and frame") "Fetches rules whose conditions may unify with the given query and frame")
(add-rules [db rule] (add-rule [db rule]
"Adds a new rule to the database")) "Adds a new rule to the database"))

View File

@ -1,41 +1,44 @@
(ns sicp-logic.db.memory (ns sicp-logic.db.memory
(:require [sicp-logic.binding :refer [instantiate var?]] (:require [sicp-logic.binding :refer [instantiate var?]]
[sicp-logic.db :refer [FactDB]])) [sicp-logic.db :refer [FactDB]]
[sicp-logic.evaluator :refer [conclusion]]))
(defn use-index? [query]
(not (var? (first query))))
(defn get-indexed-assertions [db query] (defn get-indexed-assertions [db query]
(get (deref (:index db)) (first query))) (get @(:assertion-index db) (first query)))
(defn get-all-assertions [db]
(deref (:store db)))
(defn indexable? [assertion]
(not (var? (first assertion))))
(defn index-assertion! [db assertion] (defn index-assertion! [db assertion]
(swap! (swap!
(:index db) (:assertion-index db)
(fn [index] (fn [index]
(let [index-value (or (get index (first assertion)) [])] (let [index-value (or (get index (first assertion)) [])]
(assoc index (first assertion) (conj index-value assertion)))))) (assoc index (first assertion) (conj index-value assertion))))))
(defn get-indexed-rules [db query]
(concat
(get @(:rule-index db) (first query))
(get @(:rule-index db) '?)))
(defn store! [db assertion] (defn index-rule! [db rule]
(swap! (:store db) (fn [assertions] (conj assertions assertion)))) (swap!
(:rule-index db)
(fn [index]
(let [index-key (if (var? (first (conclusion rule)))
'?
(first (conclusion rule)))
index-value (or (get index index-key) [])]
(assoc index index-key (conj index-value rule))))))
(defrecord InMemoryDB [index store] (defrecord InMemoryDB [assertion-index rule-index]
FactDB FactDB
(fetch-assertions [db query frame] (fetch-assertions [db query frame]
(let [instantiated (instantiate query frame (fn [v f] v))] (let [instantiated (instantiate query frame (fn [v f] v))]
(if (use-index? query) (get-indexed-assertions db query)))
(get-indexed-assertions db query)
(get-all-assertions db))))
(add-assertion [db assertion] (add-assertion [db assertion]
(when (indexable? assertion) (index-assertion! db assertion))
(index-assertion! db assertion)) (fetch-rules [db query frame]
(store! db assertion))) (get-indexed-rules db query))
(add-rule [db rule]
(index-rule! db rule)))
(defn new-db [] (defn new-db []
(->InMemoryDB (atom {}) (atom []))) (->InMemoryDB (atom {}) (atom {})))

View File

@ -1,6 +1,6 @@
(ns sicp-logic.evaluator (ns sicp-logic.evaluator
(:require [sicp-logic.assertions :refer [find-assertions]] (:require [sicp-logic.assertions :refer [find-assertions]]
[sicp-logic.binding :refer [instantiate]] [sicp-logic.binding :refer [instantiate var?]]
[sicp-logic.db :refer [fetch-rules]] [sicp-logic.db :refer [fetch-rules]]
[sicp-logic.match :refer [unify-match]])) [sicp-logic.match :refer [unify-match]]))
@ -60,10 +60,10 @@
(let [var-name (second var) (let [var-name (second var)
binding (get @bindings var-name)] binding (get @bindings var-name)]
(if binding (if binding
binding ['? binding]
(let [new-binding (gensym var-name)] (let [new-binding (gensym var-name)]
(swap! bindings (fn [m] (assoc m var-name new-binding))) (swap! bindings (fn [m] (assoc m var-name new-binding)))
new-binding)))) ['? new-binding]))))
rename-vars (fn rename-vars [exp] rename-vars (fn rename-vars [exp]
(cond (cond
(var? exp) (rename-var exp) (var? exp) (rename-var exp)
@ -75,12 +75,14 @@
(defn conclusion [rule] (defn conclusion [rule]
"Selects the rule's conclusion") "Selects the rule's conclusion"
(first rule))
(defn rule-body [rule] (defn rule-body [rule]
"Selects the rule's body") "Selects the rule's body"
(or (second rule) :always-true))
(defn apply-a-rule [rule query frame] (defn apply-a-rule [db rule query frame]
"Applies the `rule` to the `query` in the "Applies the `rule` to the `query` in the
`frame` by unifying the query with the rule to `frame` by unifying the query with the rule to
produce a new frame then evaluating the body produce a new frame then evaluating the body
@ -91,13 +93,14 @@
frame)] frame)]
(if (= unify-result :failed) (if (= unify-result :failed)
[] []
(qeval (rule-body clean-rule) (qeval db
(rule-body clean-rule)
[unify-result])))) [unify-result]))))
(defn apply-rules [db query frame] (defn apply-rules [db query frame]
(mapcat (mapcat
(fn [rule] (fn [rule]
(apply-a-rule rule query frame)) (apply-a-rule db rule query frame))
(fetch-rules db query frame))) (fetch-rules db query frame)))
(defn simple-query [db q input-frames] (defn simple-query [db q input-frames]
@ -119,4 +122,5 @@
(= q-type 'or) (disjoin db (rest q) input-frames) (= q-type 'or) (disjoin db (rest q) input-frames)
(= q-type 'not) (negate db (rest q) input-frames) (= q-type 'not) (negate db (rest q) input-frames)
(= q-type 'lisp-value) (lisp-value (rest q) input-frames) (= q-type 'lisp-value) (lisp-value (rest q) input-frames)
(= q-type :always-true) input-frames
:else (simple-query db q input-frames)))) :else (simple-query db q input-frames))))

View File

@ -1,38 +1,69 @@
(ns sicp-logic.match (ns sicp-logic.match
(:require [sicp-logic.binding :refer [binding-in-frame extend var?]])) (:require [sicp-logic.binding :refer [binding-in-frame extend var?]]))
(declare pattern-match) (defn depends-on? [exp var frame]
"Returns `true` if `exp` contains `var` in the context
of `frame`."
(letfn [(tree-walk [node]
(cond
(var? node) (if (= var node)
true
(let [binding-value (binding-in-frame node frame)]
(if binding-value
(tree-walk binding-value)
false)))
(and (sequential? node) (not (empty? node)))
(or (tree-walk (first node))
(tree-walk (rest node)))
:else false))]
(tree-walk exp)))
(defn extend-if-consistent [var data frame] (declare unify-match)
"Extends `frame` by binding `var` to `data` as long as this is
consistent with the bindings already in `frame`." (defn extend-if-possible [var val frame]
"Extends the frame by binding `var` to `val` unless that
results in an invalid state."
(let [binding-value (binding-in-frame var frame)] (let [binding-value (binding-in-frame var frame)]
(if binding-value (cond
(pattern-match binding-value data frame) ;; recursive call to bind any variables in the binding-value ;; If the var is already bound in the frame, attempt to unify
(extend var data frame)))) ;; its value with the new value
binding-value (unify-match binding-value val frame)
;; If the the value is a variable that is already bound, attempt
;; to unify its value with the variable currently being bound
(var? val) (let [binding-value (binding-in-frame val frame)]
(if binding-value
(unify-match var binding-value frame)
(extend var val frame)))
;; If the var is found somewhere in the val, fail, since it
;; is not possible to generally solve equations of the form
;; y = <expression involving y>
(depends-on? val var frame) :failed
:else (extend var val frame))))
(defn pattern-match [pattern data frame] (defn unify-match [pattern1 pattern2 frame]
"Matches `pattern` against `data`, returning either a new frame "Unifies `pattern1` with `pattern2` by binding variables
with the pattern variables bound or the keyword :failed if matching in `frame` such that both patterns could have the same
fails" value. Some pattern variables in either pattern may remain
unbound.
For example, (unify-match '[?a ?b foo] '[?c [?d bar] ?e] {}) yields
the new frame '{a [?c], b [?d bar], e foo}."
(cond (cond
;; If the frame has already failed, fail ;; If the unification has already failed, fail
(= frame :failed) :failed (= frame :failed) :failed
;; If the pattern already equals the data, ;; If the patterns are already equal, the frame already
;; the frame already has the correct bindings ;; has the correct bindings
(= pattern data) frame (= pattern1 pattern2) frame
;; If the pattern is a variable, try to extend the frame by binding that ;; If pattern1 is a variable, try to bind it to pattern2
;; variable to the data (var? pattern1) (extend-if-possible pattern1 pattern2 frame)
(var? pattern) (extend-if-consistent pattern data frame) ;; If pattern1 is not a variable but pattern2 is, try to bind
;; If the pattern and data are both lists, recurse into the list ;; pattern2 to pattern1
(and (sequential? pattern) (sequential? data)) (var? pattern2) (extend-if-possible pattern2 pattern1 frame)
(pattern-match ;; If both patterns are lists, recursively unify them
(rest pattern) (and (sequential? pattern1) (sequential? pattern2))
(rest data) (unify-match (rest pattern1)
(pattern-match (first pattern) (rest pattern2)
(first data) (unify-match (first pattern1)
frame)) (first pattern2)
;; Otherwise we can't match this pattern frame))
:else :failed)) :else :failed))
(defn unify-match [pattern data frame])