Using ^:dynamic *bindings* for awesome!



Using ^:dynamic *bindings* for awesome!

0 0


clojure-la-oct-bindings


On Github marshallbrekka / clojure-la-oct-bindings

Using ^:dynamic *bindings* for awesome!

Quick Intro to Bindings

  • Used with dynamic variables
  • Thread local

Simple Bindings Demo

(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

How we use bindings at ReadyForZero

Hiding complexity behind simple interfaces

Tonight's Example: Database Caching Layer

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)

How do we solve those requirements?

With a tree!

What does that look like?

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)})

What does that look like?

Functional and Data Dependencies (as a tree)

read-user <user-id>read-accounts <user-id>db/read-model :accounts, <user-id>read-payments <user-id>db/read-model :payments, <user-id>

4. DB writes invalidate dependent cached functions

Traverse the dependency tree Mark each leaf as invalid
insert :accounts, {:user-id 10}db/read-model:accounts, 10 read-accounts10 read-user10

Constructing a dynamic dependency tree with bindings

If parent context exists, register with parent Construct a new context Run the function body Save our context and return the result
(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)))

One Problem...

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)))

Wrap it in a macro!

(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#))))))

Programmer Peace of Mind

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))

Thanks For Listening

 

Marshall Brekka - marshall.brekka@avant.com

Using ^:dynamic *bindings* for awesome!