Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Extending core.logic (Datomic example)
Clone this wiki locally
Unifying Custom Data Structures
core.logic was designed with extensibility in mind. You may want to unify your own custom data structures - not only the ones provided by Clojure. To see how this can be done we'll use Datomic datums as an example.
For the following examples we assume you have created a Datomic test db and the following ns declaration:
(ns example (:use [datomic.api :only [db q] :as d] [clojure.core.logic] [clojure.core.logic.datomic :only [datom?]])) (def uri "datomic:dev://localhost:4334/test") (def conn (d/connect uri))
In order for your custom data structure to participate in unification it must implement
We can implement
IUnifyTerms For Datomic datums like so:
(extend-type datomic.Datom clojure.core.logic.protocols/IUnifyTerms (unify-terms [u v s] (unify-with-datom* u v s)))
u is of course the datum,
v is other data structure being unified with and
s is the substitutions map.
This code simply calls the actual function that we will define later. A separate function is used so that other types can reuse it to unify with Datoms without the overhead of double dispatch.
We would like unification to possibly succeed with instances of
clojure.lang.PersistentVector. This is because datums are 4 element tuples. We now implement a simple function that does this. Again returning
nil(>= 0.8.0) for failed unification.
(defn unify-with-datom* [u v s] (when (and (instance? clojure.lang.PersistentVector v) (> (count v) 1)) (loop [i 0 v v s s] (if (empty? v) s (when-let [s (unify s (first v) (nth u i))] (recur (inc i) (next v) s))))))
v will be an instance of
u will be the datum and
s will be the current substitutions map. It should be clear here that datums may only unify with instances of
clojure.lang.PersistentVector containing only 4 elements. This code simply unifies each element in the current substition - creating a new substitution which must be used for the next unification attempt. If all the elements of
u unify, we return the new (possibly changed) substitutions map.
Unification is a binary operation - we must handle the possibility of a datum appearing as the left or right operand. Note the use of
unify-with-sequential* from the
(extend-type clojure.lang.PersistentVector clojure.core.logic.protocols/IUnifyTerms (unify-terms [u v s] (if (datom? v) (unify-with-datom* v u s) (when (sequential? v) (unify-with-sequential* u v s)))))
There is nothing more to do. Datums can now participate in unification.
Custom Data Sources
In order to write any interesting relational programs against Datomic we need to be able to unify with external data sources. This can be accomplished with
(defn datomic-rel [q] (fn [a] (l/to-stream (map #(l/unify a % q) (d/datoms (db conn) :eavt)))))
to-stream creates a stream of choices suitable for core.logic's operation. It can take any seqable data structure. In this case we simply read out raw index data from Datomic. We unify each datum with the closed over argument
q. This is important - core.logic relations are just closures. They must return a function that takes a single argument (in this case
a) which is the current substitution.
You can now run core.logic queries against Datomic:
(run* [q] (fresh [e a v t] (== v true) (datomic-rel [e a v t]) (== q [e a v t])))