Lazy sequences are a great invention of functional programming.
Dynamic scope is a great invention of functional programming.
But, when you combine lazy sequnces with dynamic scope you get a complex behaviour.
This inherent complexity has been clarifed to me while reading Elements of Clojure, definitely a great book!
After all, it’s not a big surprise: in the real life, laziness and dynamism are quite opposite.
Before dealing with this complexity, let’s see how lazy sequences live in peace with lexical scope.
In Clojure, the
map function returns a lazy sequence consisting of the result of applying a function to the items of a collection.
In the following code snippet, our collection is the array
[1 2 3] and our function always returns
my-value which is part of the lexical scope specify by the
In this scenario,
map behaves in a simple way: it takes
my-value from the lexical scope and its value is 1. The global variable
my-value is overriden by the local binding of
(def my-value 42) (def my-lexical-scope-map (let [my-value 1] (mapv (fn [_] my-value) [1 2 3]))) (first my-lexical-scope-map)
So far so good.
But with dynamic scope, things get much more complicated.
Let’s map again over the
[1 2 3] array, this time with a dynamic variable
*my-value* that is overriden by the
(def ^:dynamic *my-value* 42) (def my-dynamic-scope-map (binding [*my-value* 1] (map (fn [_] *my-value*) [1 2 3]))) (first my-dynamic-scope-map)
In this situation, the value of the first element of the map is:
42. It seems like the
binding macro had no effect. This is a complex behaviour. If we use
mapv instead of
map, we get the same behaviour as with the lexical scope:
(def my-value 42) (def my-dynamic-scope-vector (binding [*my-value* 1] (mapv (fn  *my-value*) [1 2 3]))) (first my-dynamic-scope-vector)
This complex idea is that lazy sequences relies on referential transparency, which formally means that an expres sion and its result are interchangeable. Dynamic scope breaks referential transparency.
mapv behave differently is because
map returns a lazy sequence, while
mapv returns a vector. The lazy sequence elements are evaluates outside of the dynamic scope: therefore
(first my-dynamic-scope-map) is
42, while the vector elements are evaluates in the dynamic scope: therefore
(first my-dynamic-scope-vector) is
Clojure supports dynamic scope for convenience reason. For instance, in the case of testing, dynamic scope allows us to easilly mock a function. But this comes at a price. Dynamic scope is definitely not simple.
Beware of the inherent complexity of dynamic scope, each time, you rely on dynamic scope.
If you are in Clojure and not in Clojurescript, you can use bound-fn instead of the regular
fn to capture the dynamic bindings, as suggested by “Stealing Fat” in the comments below.
The code snippet will be:
(def ^:dynamic *my-value* 42) (def my-dynamic-scope-map-bound (binding [*my-value* 1] (map (bound-fn [_] *my-value*) [1 2 3]))) (first my-dynamic-scope-map-bound)
And indeed, it returns 1.
Thank you “Stealing Fat”.