Skip to content
Dynamic Vars and Bindings
step 1/5

Reading — step 1 of 5

Learn

~1 min readMacros and Dynamic Vars

Most Clojure vars are immutable from the user's perspective. Dynamic vars can be rebound in a thread-local way using binding.

(def ^:dynamic *log-level* :info)

(defn log [msg]
    (when (= *log-level* :debug)
        (println "[DEBUG]" msg))
    (when (#{:info :debug} *log-level*)
        (println "[INFO]" msg)))

(log "hello")
;; [INFO] hello

(binding [*log-level* :debug]
    (log "hello"))
;; [DEBUG] hello
;; [INFO] hello

Naming convention: *name* (earmuffs) for dynamic vars. The ^:dynamic metadata is required.

Thread-local: each thread has its own binding stack. Inside binding, the current thread's view of the var changes — other threads are unaffected.

Use cases:

  • Logging context (current request, user)
  • Database connections
  • Configuration override in tests
  • Stream redirection

Clojure's standard library has many: *out*, *in*, *ns*, *compile-files*, *print-length*, etc.

(binding [*out* (java.io.StringWriter.)]
    (println "goes to writer, not stdout")
    (.toString *out*))   ;; "goes to writer, not stdout\n"

Be careful in lazy sequences and async code — bindings are stack-based. If a lazy seq is realized OUTSIDE a binding block, the binding is gone:

(def ^:dynamic *config* nil)

(defn make-seq []
    (map (fn [n] {:n n :cfg *config*}) (range 5)))

(let [seq (binding [*config* :testing] (make-seq))]
    (first seq))
;; {:n 0 :cfg nil}   ;; binding gone — seq not realized inside

(binding [*config* :testing]
    (doall (make-seq)))
;; force realization inside the binding

alter-var-root changes the root binding (visible to all threads). Use sparingly — basically global mutable state.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…