Why are variables in list cormprehensions not considered is mutable state in clojure?

Viewed 112

In Clojure every variable is immutable. But when i use list comprehension like in the case below, the elem variable seems to be mutable, because each time elem is overwritten by 1, then by 2 and then by 3 or it is not?

(for [elem [1 2 3]] 
  elem)

Is this a point where mutability is allowed or am i missing something?

3 Answers

"Mutation" refers to an existing variable changing its contents. You could observe this if you had a reference to a variable, looked at it once, noting its value as X, and then later looked at the same variable again, noting its value is now Y. That isn't what's happening in a list comprehension.

First, let's talk about one thing that I hope you will agree is not mutation: calling a function multiple times with different values. Suppose we have

(defn triple [x]
  (* x 3))

If we write [(triple 1) (triple 2)], do we say that x has mutated? Of course not. There were two different invocations of the function triple, each with a different value for x, but those weren't the same variable: they were different instantiations of x.

A list comprehension is the same thing. The body is a function, which is evaluated once for each of the inputs. It doesn't look like a function, because there's no fn, but it really is one, both technically (it macroexpands into the body of a fn) and philosophically (it handles inputs the same way as our triple function above). (for [x xs] (f x)) is no different from writing (map f xs), which needs no mutation.

Usually when newcomers worry about mutation in Clojure, they are worried about let, which allows you to replace existing bindings:

(let [x 1
      _ (prn x)
      x 2]
  (prn x))

This prints 1 2: doesn't this prove that x has mutated? No, it doesn't: the old x is still there, it's just shadowed so you can't refer to it anymore. You can prove this by using a function to let you refer to the old x:

(let [x 1
      f (fn [] x)
      x 2]
  (prn (f) x))

This still prints 1 2 even though both prints happen after x was bound to 2. This is because f still sees the old x. The new x is an unrelated variable with the same name; you might as well have called it y and renamed all references to it.

The distinction between value and identity, as described here, is important. If an identity appears to change, it is because it becomes associated with different state values over time.

If you have a C/C++ background, it may help to think that elem behaves like a non-const pointer which points at const objects.

If you have a Python/Java background, it might help to think that the values in the collection you are traversing are immutable.

In any case, the immutability question is an assertion about the elements in the collection, not about elem.

Related