Clojure - 循环变量 - 不变性

时间:2017-01-06 17:58:10

标签: loops clojure immutability

我正在尝试在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-ncurr-f可变值类似于命令式语言中的循环变量?

2 个答案:

答案 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-ncurr-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中,尽管我认为这个问题目前没有实际意义。