Dynamic Bindings

The Clojure community loves lexical scoping, where the value of a symbol in an expression is solely determined by its placement in the code, not by its position on the call stack. That is, we like functions to accept arguments for each piece of data that may vary from one invocation to the next.

context/circle_area_lexical.clj
 
(​defn​ circle-area [radius]
 
(​*​ Math/PI (​*​ radius radius)))
 
 
(circle-area 10.0)
 
;=> 314.1592653589793

Here radius is a lexically scoped local variable: its value is determined solely by the argument passed into the function. That makes it easy to see at a glance what the dependencies are. Function parameters, along with let and loop bindings, are common examples of lexical binding.

Can you imagine why many of us prefer lexical scoping over dynamic scoping, where the values that a function uses to complete its evaluation are injected from outside of the function definition?

context/circle_area_dynamic.clj
 
(​declare​ ^:dynamic *radius*)
 
(​defn​ circle-area []
 
(​*​ Math/PI (​*​ *radius* *radius*)))
 
 
(​binding​ [*radius* 10.0]
 
(circle-area))
 
;=> 314.1592653589793

Note that the asterisks in *radius* are called earmuffs and are just a naming convention for dynamically scoped vars in Clojure—they’re not required by the language.

Despite the conventional preference for lexical scoping, there are good reasons that Clojure has dynamic scope as an option. It gives us an escape hatch when it would be too cumbersome to pass values through a chain of functions that are otherwise oblivious to the value some lower-level function needs. Prime examples of this convenience are the I/O vars *out*, *in*, and *err*. In Unix, we’re used to the idea of changing the source and destination of the stdout, stdin, and stderr streams by using pipes and command-line redirection. In scoping terms, rebinding these Clojure vars is very similar to how we redirect Unix input and output streams to wire programs together.

Whenever we use any of Clojure’s printing functions, we’re actually using dynamic bindings:

context/log.clj
 
(​defn​ log [message]
 
(​let​ [timestamp (​.​​format​ (java.text.SimpleDateFormat. ​"yyyy-MM-dd'T'HH:mmZ"​)
 
(java.util.Date.))]
 
(​println​ timestamp ​"[INFO]"​ message)))

It’s nice for log to be able to call println, which uses *out*, without having to know what *out* is actually pointing to. This way, log only has to know about println, not println’s dependency *out*. So if we want to vary where things are printed, we can just rebind *out* whenever we want to call code that uses log.

context/log_to_file.clj
 
(​defn​ process-events [events]
 
(​doseq​ [event events]
 
;; do some meaningful work based on the event
 
(log (​format​ ​"Event %s has been processed"​ (:id event)))))
 
 
(​let​ [file (java.io.File. (System/getProperty ​"user.home"​) ​"event-stream.log"​)]
 
(​with-open​ [file (clojure.java.io/writer file :append true)]
 
(​binding​ [*out* file]
 
(process-events [{:id 88896} {:id 88898}]))))

Here we’ve decided to re-route *out* to a log file since we may want to take a look at it later. This works just fine, but it’s kind of noisy, isn’t it? If we wanted to do this in several places in code, we’d need to repeat all this wordy code, and that’d be a shame. Besides, the main point of this code is to process events, but that intent is buried inside these with-open and binding expressions that set up the output stream. With a couple minutes of investment, we can write a macro to abstract that pattern away:

context/with_out_file.clj
 
(​defmacro​ with-out-file [file & body]
 
`(​with-open​ [writer# (clojure.java.io/writer ~file :append true)]
 
(​binding​ [*out* writer#]
 
~@body)))
 
 
(​let​ [file (java.io.File. (System/getProperty ​"user.home"​) ​"event-stream.log"​)]
 
(with-out-file file
 
(process-events [{:id 88894} {:id 88895} {:id 88897}])
 
(process-events [{:id 88896} {:id 88898}])))

This feels more aligned with the problem domain. We no longer have to specify the details of how the output stream is created and bound, just the file where we want to append output. Setting up bindings like those in with-out-file is a very common use case for macros in the wild in real-world scenarios. Clojure itself has several built-in macros that are similar to with-out-file. The most similar is probably with-out-str:

context/with_out_str.clj
 
(​defmacro​ ​with-out-str
 
"Evaluates exprs in a context in which *out* is bound to a fresh
 
StringWriter. Returns the string created by any nested printing
 
calls."
 
{:added ​"1.0"​}
 
[& body]
 
`(​let​ [s# (new java.io.StringWriter)]
 
(​binding​ [*out* s#]
 
~@body
 
(​str​ s#))))

Looks kind of familiar, right? with-out-str evaluates the body form with *out* rebound, in order to collect output and return it as a string. This is very useful for unit testing I/O produced by a function, along with its sibling with-in-str, which does something similar for input:

context/with_in_str.clj
 
(​defmacro​ ​with-in-str
 
"Evaluates body in a context in which *in* is bound to a fresh
 
StringReader initialized with the string s."
 
{:added ​"1.0"​}
 
[s & body]
 
`(​with-open​ [s# (​->​ (java.io.StringReader. ~s)
 
clojure.lang.LineNumberingPushbackReader.)]
 
(​binding​ [*in* s#]
 
~@body)))
 
 
(​defn​ join-input-lines [separator]
 
(​print​ (clojure.string/replace (​slurp​ *in*) ​" "​ ​","​)))
 
 
(​let​ [result (​with-in-str​ ​"hello there hi sup ohai"
 
(​with-out-str
 
(join-input-lines ​","​)))]
 
(​assert​ (​=​ ​"hello there,hi,sup,ohai"​ result)))

Even though the function join-input-lines acts directly on *in* and *out*, we’re able to set up fake versions of those streams that allow us to inject the input we want and to look at the output to verify what happened. And this is all because those macros allowed us to evaluate join-input-lines in a context of our choosing.

Now’s a great time to go back to our duplication-heavy code from the start of the chapter and write a macro to remove the duplication.

A Non-Macro Approach

There are other ways besides macros to solve the problem of contextual evaluation, however. A version of with-out-file built only with functions is also a reasonable option if we’re willing to modify the calling syntax slightly:

context/with_out_file_fn.clj
 
(​defn​ with-out-file [file body-fn]
 
(​with-open​ [writer (clojure.java.io/writer file :append true)]
 
(​binding​ [*out* writer]
 
(body-fn))))
 
 
(​let​ [file (java.io.File. (System/getProperty ​"user.home"​) ​"event-stream.log"​)]
 
(with-out-file file
 
(​fn​ []
 
(process-events [{:id 88894} {:id 88895} {:id 88897}])
 
(process-events [{:id 88896} {:id 88898}]))))

This is actually not bad at all. There aren’t that many differences in this case, and it’s not clear that one is inherently better than the other from a syntax perspective. From a code concision perspective, the macro version is nicer—we don’t need the (fn [] ...) wrapping the input code—but there are always trade-offs. For example, if we wanted to pass with-out-file around as a value (a higher-order function), we wouldn’t be able to use the macro version.

In general, a macro that consumes code-to-be-evaluated can usually be re-written as a function that takes a thunk—a function with no arguments that delays code evaluation. This macro-replacing function can choose to evaluate the thunk multiple times, or not at all, just like the macro could. The cost is a small one in code clarity on the caller’s side, but I suspect it’s why with-* macros seem to be so common in practice despite the fact that they could be replaced by functions. Using a macro allows your callers to avoid the (fn [] ...) boilerplate that a higher-order function would require.

Luckily, we can get the best of both worlds by wrapping a macro as a thin wrapper around the function version:

context/with_out_file_fn_wrapper.clj
 
(​defn​ with-out-file-fn [file body-fn]
 
(​with-open​ [writer (clojure.java.io/writer file :append true)]
 
(​binding​ [*out* writer]
 
(body-fn))))
 
 
(​defmacro​ with-out-file [file & body]
 
`(with-out-file-fn ~file
 
(​fn​ [] ~@body)))
 
 
(​let​ [file (java.io.File. (System/getProperty ​"user.home"​) ​"event-stream.log"​)]
 
(with-out-file file
 
(process-events [{:id 88894} {:id 88895} {:id 88897}])
 
(process-events [{:id 88896} {:id 88898}])))

This way, users have the choice to use the more concise macro version or the more flexible function version. When this style of wrapping is possible, it’s pretty much always a good idea. Giving your teammates and library users (and your future self!) the option to use a function when they want to is just a nice thing to do.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.196.146