I think the article is a great explanation. I'm not sure I (exactly) agree with the conclusion "dynamic vars also breaks referential transparency" - isn't it the combination of lexical closure (referring to symbols from outside the function) as well as mutable state that breaks referential transparency?
If I have a function that takes a java collection and returns the count, it has no referential transparency because the collection is mutable, not necessarily b/c of how it's operating on its arguments.
Of course this is one of the things I love about Clojure and Rich Hickey's use of immutability as the default behavior (as often as possible) - much [more] of the Clojure I write has referential transparency, hardly any of the Java I wrote did.
Dynamically bound values can be thought of as invisible arguments, that are passed through every function up the call stack. A referentially-transparent function only depends on its explicit arguments, and will always return the same value when passed the same arguments. A function that depends on a dynamically-bound variable may return different values when called with the same arguments.
Example (Common Lisp syntax, not actually tested):
(defvar z 0)
(defun example (x y)
(declare (special z))
(+ x y z))
(example 1 2)
==> 3
(let ((z 10))
(example 1 2))
=> 13
In this sample, the function example is not referentially transparent, because it yields different results when passed the same argument. Note that this happens without using closures or mutation. example does not close over the value of z in any way because it is dynamically scoped[1]. There is no mutation because z is re-bound, which is conceptually different from mutation. It's effectively creating a new binding with the same name and different scope; a function called within that scope will look for the value by name, and find the new binding. The original binding is untouched outside of this new scope.
[1] This is tautological; The term "closure" is defined to refer to lexical scoping, and was invented to describe it[2].
[2] Actually, now that I'm writing this, it occurred to me that your confusion entirely stems from subtleties in the definitions of lexical closure and bindings. A closure is not any function that refers to symbols outside its body. The term only refers to functions that use lexical scoping to do so, and therefore need to "close over" their surrounding data and carry it around with them. Functions referencing dynamically scoped variables do not need to carry their data around, because they look up the call stack every time.
To your point, it is "referring to symbols outside the function" that breaks referential transparency, but dynamic bindings do so in an orthogonal fashion from closures, and unlike closures do not require mutation to do so.
The function that creates the lexical closure can still have referential transparency. For example:
(defn make-adder [x y]
(fn [z] (+ x y z)))
((make-adder 1 2) 3)
-----VS-------------
(def ^{:dynamic} x 1)
(def ^{:dynamic} y 2)
(defn make-adder []
(fn [z] (+ x y z)))
((make-adder) 3)
In the first example, the closure returned is still a function of the arguments passed to make-adder, and it is easy to reason about. In the second example, the closure returned relies on the dynamic value of x and y at run time. I agree that if you close over a reference to a mutable value you can also break referential transparency, but that's the exception and not the rule with clojure since it is immutable by default. With dynamic vars you are almost guaranteed to break referential transparency.
The GHC Haskell compiler also supports dynamically scoped parameters via the XImplicitParams switch [1]. Type constraints are used to indicate which variables are propagated to the function.
I've used implicit params in my toy compiler code. While it somewhat simplified the structure of the code, I could have easily written the program without them.
If I have a function that takes a java collection and returns the count, it has no referential transparency because the collection is mutable, not necessarily b/c of how it's operating on its arguments.
Of course this is one of the things I love about Clojure and Rich Hickey's use of immutability as the default behavior (as often as possible) - much [more] of the Clojure I write has referential transparency, hardly any of the Java I wrote did.
Thanks!
Kyle