Reputation: 903
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
Reputation: 13483
Isn't
curr-n
andcurr-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
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