我正在尝试在Clojure中学习函数式编程。许多函数式编程教程都从不变性的好处开始,一个常见的例子是命令式语言中的循环变量。在这方面,Clojure的loop-recur
与他们有什么不同?例如:
(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
和curr-f
可变值类似于命令式语言中的循环变量?
答案 0 :(得分:3)
正如Thumbnail所指出的,在clojure中使用loop-recur
具有与经典递归函数调用相同的形式和效果。它存在的唯一原因是它比纯递归更有效。
由于recur
只能出现在尾部位置,因此您将保证永远不再需要loop
“变量”。因此,您不需要在堆栈上保留它们,因此不使用堆栈(与嵌套函数调用不同,递归与否)。最终的结果是它看起来和与其他语言中的命令循环非常相似。
与Java风格的for
循环相比,改进是所有“变量”仅限于在{{中}初始化时“更改” 1}}表达式以及在loop
表达式中更新时的表达式。在循环体中不会发生对变量的更改,也不会发生其他任何变化(例如嵌入式函数调用可能会改变Java中的循环变量)。
由于对“循环变量”可以变异/更新的位置的这些限制,因此减少了无意中更改它们的机会。限制的代价是循环不像传统的Java风格循环那样灵活。
与任何事情一样,由您决定何时这种成本 - 收益权衡是比其他可用的成本效益权衡更好的选择。如果你想要一个纯Java风格的循环,很容易使用clojure recur
来模拟Java变量:
atom
即使在我们使用原子来模拟Java中的可变变量的最后一种情况下,至少每个地方我们都会变异,它会被; 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
函数明显标记,这使得更难以错过“意外”变异。
P.S。如果您希望使用swap!
,则in the Tupelo library
答案 1 :(得分:2)
Isn&#39; t
curr-n
和curr-f
可变值类似于循环变量in 命令式语言?
没有。您始终可以将loop
- recur
重写为递归函数调用。例如,您的factorial
函数可以重写......
(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))
速度较慢,大数字堆栈溢出。
当涉及到实现呼叫的那一刻时,recur
会覆盖唯一的堆栈帧,而不是分配新的堆栈帧。这仅适用于此后从未引用调用者的堆栈帧 - 我们称之为尾部位置。
loop
是句法糖。我怀疑它是一个宏,但它可能是。除了早期的绑定应该可以用于后面的绑定,例如在let
中,尽管我认为这个问题目前没有实际意义。