As I get back into writing Lisp code — via Clojure — I am finding a few things that I’m tripping up over on a regular basis. Some of this is pre-conceived notions of behavior, and some is just mental laziness. The one I keep coming back to, over and over, is the subtleties of lexical scoping versus dynamic scoping. Inspired by this post, I thought I’d take a stab at explaining it in a slightly different way. You can never have too many explanations. This is my understanding as it exists today. It is likely somewhat wrong. It might even be completely and totally wrong.
The first thing to do is replace the word “lexical” with the word “static”. Now we’re using orthogonal vocabulary. It also makes some things a bit more obvious. Before we dig into scoping (static v. dynamic), we first need to be careful to define what we’re talking about: bindings. The idea of a “variable” doesn’t really exist in the traditional sense. You have symbols, values and bindings. Take the following code snippet for example:
user=> (def foo)
#'user/foo
Here, we have def, which is a special form that creates a Var, and foo, which is a symbol that is used to identify the Var in the future. At this point, foo has no “value”, and so we can’t refer to it:
user=> foo
java.lang.IllegalStateException: Var user/foo is unbound.
So why in the world would you want to do this? Just you wait and see. More typically, you also create the initial binding to a value:
user=> (def foo 100)
#'user/foo
Once again, we have the symbol foo, but it’s now bound to the value 100. The binding is that “invisible” link between the Var and the value. To quote the Clojure “manual”:
Every Var can (but needn’t) have a root binding, which is a binding that is shared by all threads that do not have a per-thread binding. Thus, the value of a Var is the value of its per-thread binding, or, if it is not bound in the thread requesting the value, the value of the root binding, if any.
That seems easy enough, right? Right. So, if you saw the following code snippet, what would you think we would get in response?
user=> (println "The value of foo is:" foo)
Easy, right? The value of foo is: 100. Simple, and no strange behavior, right? Right. Next up, static (lexical) scoping. This is best typified by the let special form.
user=> (let [foo 50] (println "The value of foo is:" foo))
Also, not too hard, I suppose. We get The value of foo is: 50, which is pretty predictable. So what’s the fuss? Well, take a look at the next code sample and see if you can figure out what the output is.
(def foo 100)
(defn print-foo []
(println "The value of foo is:" foo))
(let [foo 50]
(print-foo))
Did you answer 50? That’s unfortunately wrong. The answer is 100, but why? Let’s return to Wikipedia:
With lexical scope, a name always refers to its (more or less) local lexical environment. This is a property of the program text and is made independent of the runtime call stack by the language implementation.
When Clojure evaluated the defn form, it used the Var that was available at-the-time. When you use the let special form, you “shadow” the original Var — creating a hierarchy of bindings — but you don’t change the binding that print-foo was referring to. Hence, 100. In the other example, we don’t refer to the Var until inside the let special form.
Another bit about let that some poeple might not pick up is that once assigned, the value of the local can never change. The binding is fixed. For example:
user=> (let [foo 100]
(println "The value of foo is:" foo)
(def foo 200)
(println "The value of foo is:" foo))
The value of foo is: 100
The value of foo is: 100
Wait, what? As I said, you can’t rebind foo inside the let form. It is a “constant”. Another behavior of let is that it performs its bindings sequentially.
user=> (let
[foo 100
bar foo]
(println "The value of foo is:" foo)
(println "The value of bar is:" bar))
The value of foo is: 100
The value of bar is: 100
So that covers the basics of the let special form.
Next, we have the binding special form, and it’s a tad more complicated to understand. So let’s first take the API documentation:
Creates new bindings for the (already-existing) vars, with the supplied initial values, executes the exprs in an implicit do, then re-establishes the bindings that existed before. The new bindings are made in parallel (unlike let); all init-exprs are evaluated before the vars are bound to their new values.
So, first, the Var must already exist. You can’t do this:
user=> (binding [bar 100] (println "The value of bar is:" bar))
java.lang.Exception: Unable to resolve var: bar in this context
So how might we solve this? Well, if we don’t care about the root binding, but only the binding that exists within the binding special form, then we could do something like this:
user=> (def bar)
#'user/bar
user=> (binding [bar 100] (println "The value of bar is:" bar))
The value of bar is: 100
Recall, that def without a value just creates an unbound Var. If we had tried to refer to it prior to binding it, then we would have gotten an exception. I’m not sure this is a wise way to use things, but in my mind binding is primarily something to be used in a concurrent environment since any binding is always thread-local.
So what does binding do? It replaces the root binding of the Var with a thread-local binding for anything inside the form. So, for example:
user=> (def bar 10)
#'user/bar
user=> bar
10
user=> (binding [bar 100]
(println "The value of bar is:" bar))
The value of bar is: 100
user=> bar
10
As you can see, bar returns back to it’s original value outside the form. Now, let’s play with it in a similar way to how we did with let earlier.
user=> (defn print-bar [] (println "The value of bar is:" bar))
#'user/print-bar
user=> (print-bar)
The value of bar is: 10
user=> (binding [bar 100] (print-bar))
The value of bar is: 100
In this case, we’ve swapped the root binding out from under the original one and we’re not actually shadowing it as we would with let. Another difference is that binding performs its initial bindings in parallel, which means you can’t have dependencies on earlier bindings made in the same form.
user=> (def baz 20)
#'user/baz
user=> (defn print-baz [] (println "The value of baz is:" baz))
#'user/print-baz
user=> (binding [bar 100 baz bar]
(print-bar)
(print-baz))
The value of bar is: 100
The value of baz is: 10
There’s one last little piece, and that’s the behavior of anonymous functions inside let versus their behavior inside of binding.
user=> (binding [print-bar print-baz] #(print-bar))
#<user$eval__1620$fn__1621 user$eval__1620$fn__1621@144683c2>
Wait, what just happened? Why didn’t it print something? That’s simple. The creation of the anonymous function doesn’t call the function, so the result of the form is the function, not the value of calling the function. So that’s point one. Point two is that if you look at what was returned, say by capturing it in another Var, you’ll see that it’s the original print-bar, not the rebound one.
user=> (def print-what (binding [print-bar print-baz] #(print-bar)))
#'user/print-what
user=> (print-what)
The value of bar is: 10
Why might that be? It’s because the anonymous function was actually evaluated “outside” the binding. This is because it’s actually created by the reader macros. So, what do you do if you want the binding to apply?
user=> (binding [print-bar print-baz] (#(print-bar)))
The value of baz is: 20
In this situation, the extra set of parenthesis force the evaluation of the anonymous function inside the binding.
Hopefully, this has been of some use to some people. It’s been useful for me in thinking through the differences and the uses of the two forms. As with the original post, I come away with a few specific guidelines.
- Use
let unless you really know you need binding.
- Use
binding when you need a thread-local version of a Var.
let is assigns sequentially, but binding is parallel.
If I’ve made any mistake, no matter how minor, please correct me in the comments, and I’ll update this to reflect it. This is an exceptionally important, but also subtle, area of languages.
Tagged: clojure, lisp