Redefining a let'd variable in Clojure loop

MBCook picture MBCook · Jun 2, 2009 · Viewed 14.6k times · Source

OK. I've been tinkering with Clojure and I continually run into the same problem. Let's take this little fragment of code:

(let [x 128]
  (while (> x 1)
    (do
      (println x)
      (def x (/ x 2)))))

Now I expect this to print out a sequence starting with 128 as so:

128
64
32
16
8
4
2

Instead, it's an infinite loop, printing 128 over and over. Clearly my intended side effect isn't working.

So how am I supposed to redefine the value of x in a loop like this? I realize this may not be Lisp like (I could use an anonymous function that recurses on it's self, perhaps), but if I don't figure out how to set variable like this, I'm going to go mad.

My other guess would be to use set!, but that gives "Invalid assignment target", since I'm not in a binding form.

Please, enlighten me on how this is supposed to work.

Answer

Brian Carper picture Brian Carper · Jun 2, 2009

def defines a toplevel var, even if you use it in a function or inner loop of some code. What you get in let are not vars. Per the documentation for let:

Locals created with let are not variables. Once created their values never change!

(Emphasis not mine.) You don't need mutable state for your example here; you could use loop and recur.

(loop [x 128]
  (when (> x 1)
    (println x)
    (recur (/ x 2))))

If you wanted to be fancy you could avoid the explicit loop entirely.

(let [xs (take-while #(> % 1) (iterate #(/ % 2) 128))]
  (doseq [x xs] (println x)))

If you really wanted to use mutable state, an atom might work.

(let [x (atom 128)]
  (while (> @x 1)
    (println @x)
    (swap! x #(/ %1 2))))

(You don't need a do; while wraps its body in an explicit one for you.) If you really, really wanted to do this with vars you'd have to do something horrible like this.

(with-local-vars [x 128]
  (while (> (var-get x) 1)
    (println (var-get x))
    (var-set x (/ (var-get x) 2))))

But this is very ugly and it's not idiomatic Clojure at all. To use Clojure effectively you should try to stop thinking in terms of mutable state. It will definitely drive you crazy trying to write Clojure code in a non-functional style. After a while you may find it to be a pleasant surprise how seldom you actually need mutable variables.