September 11th, 2015
(written by lawrence krubner, however indented passages are often quotes). You can contact lawrence at: firstname.lastname@example.org
The interesting thing here is that the dynamic vars are wrapped in functions, which makes them a bit safer than dynamic scope would imply.
Each of these sub-clauses is very similar to the parent structures, and can be arbitrarily nested. As such, it is most easily constructed with a recursive function. However, Clojure’s let bindings are lexical, and don’t extend into recursive calls. We could make the memoized gensym call a parameter, but since we also need a separate generator for cluster names, we can make our parameter list much simpler by just using Clojure’s dynamic binding capabilities.(def ^:private ^:dynamic *node->id* nil) (def ^:private ^:dynamic *cluster->id* nil) (defn- node->id [n] (*node->id* n)) (defn- cluster->id [s] (*cluster->id* s)) (defmacro ^:private with-gensyms [& body] `(binding [*node->id* (or *node->id* (memoize (fn [_#] (gensym "node")))) *cluster->id* (or *cluster->id* (memoize (fn [_#] (gensym "cluster"))))] ~@body))
Here, we define two vars, which within the scope of with-gensyms will hold the memoized functions. Since these vars are an implementation detail, we interact with them via the node->id and cluster->id functions. This isn’t strictly necessary, but it leaves the door open for validation, better error messages, and other nice affordances. Generally, there should always be a function between thread-local vars and other code.
Finally, in with-gensyms we define thread-local values for the two vars, but first check if they’re already non-nil, in which case we simply pass the old value through. This means that the memoized functions are only created once, and then propagated down the call stack. Like the let binding, once we exit the outermost scope, the functions are free to be collected.