Search code examples
clojure

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


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?


Solution

  • "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.