blackened
blackened

Reputation: 903

Clojure - Loop Variables - Immutability

I am trying to learn functional programming in Clojure. Many functional programming tutorials begin with the benefits of immutability, and one common example is the loop variable in imperative-style languages. In that respect, how does Clojure's loop-recur differ from them? For example:

(defn factorial [n]
  (loop [curr-n n curr-f 1]
    (if (= curr-n 1)
      curr-f
      (recur (dec curr-n) (* curr-f curr-n)))))

Isn't curr-n and curr-f mutable values similar to loop variable in imperative-style languages?

Upvotes: 3

Views: 392

Answers (2)

Thumbnail
Thumbnail

Reputation: 13483

Isn't curr-n and curr-f mutable values similar to loop variable in imperative-style languages?

No. You can always rewrite a loop-recur as a recursive function call. For example, your factorial function can be rewritten ...

(defn factorial [n]
  ((fn whatever [curr-n curr-f]
     (if (= curr-n 1)
       curr-f
       (whatever (dec curr-n) (* curr-f curr-n))))
   n 1))

This is slower and subject to stack-overflow on big numbers.


When it comes to the moment of incarnating the call, recur overwrites the one-and-only stack frame instead of allocating a new one. This works only if the caller's stack frame is never thereafter referred to - what we call tail position.

loop is syntactic sugar. I doubt that it is a macro, but it could be. Except that the earlier bindings should be available to the later ones, as in a let, though I think this issue is currently moot.

Upvotes: 2

Alan Thompson
Alan Thompson

Reputation: 29984

As Thumbnail points out, using loop-recur in clojure has the same form and effect as a classic recursive function call. The only reason it exists is that it is much more efficient than pure recursion.

Since the recur can only occur in the tail position, you are guarenteed that the loop "variables" will never be needed again. Thus, you don't need to preserve them on the stack, so no stack is used (unlike nested function calls, recursive or not). The end result is that it looks & acts very similarly to an imperative loop in other languages.

The improvement compared to a Java-style for loop is that all "variables" are limited to "changing" only when initialized in the loop expression and when updated in the recur expression. No changes to the vars can occur in the body of the loop, nor anywhere else (such as embedded function calls which could mutate the loop vars in Java).

Because of these restrictions on where the "loop vars" can be mutated/updated, there are reduced opportunities for a bug to change them unintentionally. The cost of the restrictions is that the loop is not as flexible as a traditional Java-style loop.

As with anything, it is up to you to decide when this cost-benefit tradeoff is a better choice than the other cost-benefit tradeoffs available. If you want a pure Java-style loop, it is easy to use a clojure atom to simulate a Java variable:

; Let clojure figure out the list of numbers & accumulate the result
(defn fact-range [n]
  (apply * (range 1 (inc n))))
(spyx (fact-range 4))

; Classical recursion uses the stack to figure out the list of
; numbers & accumulate the intermediate results
(defn fact-recur [n]
  (if (< 1 n)
    (* n (fact-recur (dec n)))
    1))
(spyx (fact-recur 4))

; Let clojure figure out the list of numbers; we accumulate the result
(defn fact-doseq [n]
  (let [result (atom 1) ]
    (doseq [i (range 1 (inc n)) ]
      (swap! result * i))
    @result ))
(spyx (fact-doseq 4))

; We figure out the list of numbers & accumulate the result
(defn fact-mutable [n]
  (let [result (atom 1)
        cnt    (atom 1) ]
    (while (<= @cnt n)
      (swap! result * @cnt)
      (swap! cnt inc))
    @result))
(spyx (fact-mutable 4))

(fact-range 4) => 24
(fact-recur 4) => 24
(fact-doseq 4) => 24
(fact-mutable 4) => 24

Even in the last case where we use atoms to emulate mutable variables in Java, at least each place we mutate something it is visibly marked with the swap! function, which makes it harder to miss "accidental" mutation.

P.S. If you wish to use spyx it is in the Tupelo library

Upvotes: 4

Related Questions