Skip to content

Datomic database functions used from normal code

Petter Eriksson edited this page Dec 5, 2017 · 9 revisions

An old namespace we used for our first product is the eponai.common.database.functions namespace.

It contains a macro for defining functions that can be used from ClojureScript and Clojure as well as a Datomic function. Datomic functions is that they are just data. That they can be sent over the wire, stored in Datomic, read back out and executed. You can create Datomic functions with the datomic.api/function call which takes code, params, language and which namespaces to require. The problem is that you can't freely split functions apart and reuse functionally, as you normaly do with functions. There's (to my knowledge) no way to compose these functions. So we created a def-dbfn macro to allow us to define these functions like clojure.core/defn, use them from Clojure and ClojureScript as well as get a composed datomic.api/function.

Composing datomic functions

The eponai.common.database.functions/def-dbfn macro is similar to clojure.core/defn but it takes a map of :requires (namespaces to require) and :deps, which is a sequence of other functions that have been defined with def-dbfn.

In this example we'll define 2 functions, silent-cas and silent-cas-update.

  • silent-cas compares the current value for an attribute of an entity, and if they are equal, sets a new value.
  • silent-cas-update compares the current value with a passed one, sets the new value by running a function on the current value.
(def-dbfn silent-cas {:requires ['[datomic.api]]}
  [db entity attr value new-val]
  (when (= value (get (datomic.api/entity db entity) attr))
    [[:db/add entity attr new-val]]))

(comment
  ;; Callable from Clojure or ClojureScript as it's just a function.
  (silent-cas db 1 :foo/bar 2 3)
  
  ;; Can return a `datomic.api/function` by calling dbfn on it.
  (dbfn silent-cas) 
  ;; outputs =>
  #db/fn{:lang :clojure,
         :imports [],
         :requires [[datomic.api]],
         :params [db entity attr value new-val],
         :code "(when (= value (get (datomic.api/entity db entity) attr)) [[:db/add entity attr new-val]])"})

(def-dbfn silent-cas-update 
          {:requires ['[datomic.api]]
           :deps     [{:dbfn silent-cas
                       ;; Uses this symbol for a let to surround the body of this code.
                       ;; Was originally used to use different implementations of the var.
                       :provides 'silent-cas
                       ;; Can optionally memoize the function it depends on.
                       :memoized? false}]}
  [db entity attr value f]
  (silent-cas db entity attr value (f (get (datomic.api/entity db entity) attr))))

(comment
  (dbfn silent-cas-update)
  ;; outputs =>
  ;; The silent-cas function is inlined in the let expression.
  #db/fn{:lang :clojure,
         :imports [],
         :requires [[datomic.api]],
         :params [db entity attr value f],
         :code "(clojure.core/let [silent-cas (clojure.core/cond-> (clojure.core/fn [db entity attr value new-val] (do (clojure.core/require (quote [datomic.api]))) (when (= value (get (datomic.api/entity db entity) attr)) [[:db/add entity attr new-val]])) (clojure.core/or false false) (clojure.core/memoize))] (silent-cas db entity attr value (f (get (datomic.api/entity db entity) attr))))"})

Note: after having written the example, I realize that you can't pass a function - as I have in the examples - to the database function. Hopefully you get the idea anyway.