As you already noticed, the fact that mutability is discouraged in Clojure does not mean that it is forbidden and that there are no constructs that support it.
So you are right that using def
you can change / mutate a binding in the environment in way similar to what assignment does in other languages (see the Clojure documentation on vars). By changing bindings in the global environment you also change data objects that use these bindings. For example:
user=> (def x 1)
#'user/x
user=> (defn f [y] (+ x y))
#'user/f
user=> (f 1)
2
user=> (def x 100)
#'user/x
user=> (f 1)
101
Notice that after redefining the binding for x
, function f
has changed as well, because its body uses that binding.
Compare this with languages in which redefining a variable does not delete the old binding but only shadows it, i.e. it makes it invisible in the scope that comes after the new definition. See what happens if you write the same code in the SML REPL:
- val x = 1;
val x = 1 : int
- fun f y = x + y;
val f = fn : int -> int
- f 1;
val it = 2 : int
- val x = 100;
val x = 100 : int
- f 1;
val it = 2 : int
Notice that after the second definition of x
, the function f
still uses the binding x = 1
that was in scope when it was defined, i.e. the binding val x = 100
does not overwrite the previous binding val x = 1
.
Bottomline: Clojure allows to mutate the global environment and redefine bindings in it. It would be possible to avoid this, as other languages like SML do, but the def
construct in Clojure is meant to access and mutate a global environment. In practice, this is very similar to what assignment can do in imperative languages like Java, C++, Python.
Still, Clojure provides lots of constructs and libraries that avoid mutation, and you can come a long way without using it at all. Avoiding mutation is by far the preferred programming style in Clojure.