On Github marshallbrekka / clojure-la-oct-bindings
(ns my-ns) (def ^:dynamic *foo* "hello") (defn print-foo [] (println *foo*))
(ns your-ns (:require [my-ns])) (binding [my-ns/*foo* "world"] (my-ns/print-foo)) ;; world (my-ns/print-foo) ;; hello
Hiding complexity behind simple interfaces
Tonight's Example: Database Caching Layer
Requirements for a safe to use interface
Any function can be cached (within reason) Dependencies are not declared by the programmer Cached functions can call other cached functions (1) DB writes invalidate all dependent cached functions (2) (3)With a tree!
Functional and Data Dependencies (as code)
(ns app.user (:require [app.db :as db])) (defn read-accounts [user-id] (db/read-model :accounts {:user-id user-id})) (defn read-payments [user-id] (db/read-model :payments {:user-id user-id})) (defn read-user [user-id] {:accounts (read-accounts user-id) :payments (read-payments user-id)})
Functional and Data Dependencies (as a tree)
(ns app.cache (:require [app.cache.meta :as meta])) ;; will be nil or an atom containing a list (def ^:dynamic *dependencies* nil) ;; register with parent (defn log-dependency "Logs the key as a dependency of the parent cached context, if there is one." [dependency-key] (when *dependencies* (swap! *dependencies* conj dependency-key))) ;; Save context and return the result (defn save-dependencies [dependency-key] (meta/save dependency-key *dependencies))
(ns app.user (:require [app.db :as db] [app.cache :as cache])) (defn read-accounts [user-id] (db/read-model :accounts {:user-id user-id}))
(ns app.user (:require [app.db :as db] [app.cache :as cache])) (defn read-accounts [user-id] ;; register with parent (cache/log-dependency :read-accounts) (db/read-model :accounts {:user-id user-id}))
(ns app.user (:require [app.db :as db] [app.cache :as cache])) (defn read-accounts [user-id] ;; register with parent (cache/log-dependency :read-accounts) ;; Construct a new context (binding [cache/*dependencies* (atom '())] (db/read-model :accounts {:user-id user-id})))
(ns app.user (:require [app.db :as db] [app.cache :as cache])) (defn read-accounts [user-id] ;; register with parent (cache/log-dependency :read-accounts) ;; Construct a new context (binding [cache/*dependencies* (atom '())] (let [accounts ;; Run the function body (db/read-model :accounts {:user-id user-id})] accounts)))
(ns app.user (:require [app.db :as db] [app.cache :as cache])) (defn read-accounts [user-id] ;; register with parent (cache/log-dependency :read-accounts) ;; Construct a new context (binding [cache/*dependencies* (atom '())] (let [accounts ;; Run the function body (db/read-model :accounts {:user-id user-id})] ;; Save context and ;; return the result (cache/save-dependencies :read-accounts) accounts)))
This is fugly :(
(ns app.user (:require [app.db :as db] [app.cache :as cache])) (defn read-accounts [user-id] (cache/log-dependency :read-accounts) (binding [cache/*dependencies* (atom '())] (let [accounts (db/read-model :accounts {:user-id user-id})] (cache/save-dependencies :read-accounts) accounts)))
(defmacro defcachedfn [fn-name fn-args & body] (let [fn-key (gen-cache-key fn-name fn-args)] `(fn [args#] (if-let [cached# (get-cached ~fn-key)] cached# ;; register with parent (log-dependency ~fn-key) ;; Construct a new context (binding [*dependencies* (atom '())] ;; Run the function body (let [result# (do ~@body)] ;; Save context and ;; return the result (save-dependencies ~fn-key) result#))))))
Without caching
(defn read-accounts [user-id] (db/read-model :accounts user-id))
With caching
(defcachedfn read-accounts [user-id] (db/read-model :accounts user-id))
Marshall Brekka - marshall.brekka@avant.com