Thursday, June 28, 2012

Making variable dynamic in Clojure

As a full-time Ruby developer studying Clojure I was curious how one can monkey-patch 3rd-party's code in the runtime. The answer is to utilize Clojure's dynamic scoping to redefine a variable that holds a reference to the function of interest. But starting from version 1.3 only variables that are explicitly tagged by ^:dynamic meta tag can be dynamically bind (usage of lexical scoping has positive performance impact).

So, maybe we could change variable metadata to make the variable dynamic. Unfortunately, changing metadata using alter-meta! doesn't trigger its re-evaluation. Consider the following:

user=> (def x 42)
#'user/x

user=> (alter-meta! #'x assoc :dynamic true)
{:ns #, :name x, :dynamic true, :line 1, :file "NO_SOURCE_PATH"}

user=> (.isDynamic #'x)
false

user=> (binding [x 32] (println x))
IllegalStateException Can't dynamically bind non-dynamic var: user/x clojure.lang.Var.pushThreadBindings (Var.java:353)

Instead we can call .setDynamic directly:

user=> (.setDynamic #'x)
#'user/x

user=> (binding [x 32] (println x))
32
nil

If you want to redefine variables temporarily, without making them dynamic (e.g., for mocking out functions during testing), you can use new functions that were added to cover such use case - with-redefs-fn and with-redefs.