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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With