Clojure - 循环变量 - 不变性

Clojure - Loop Variables - 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)))))

curr-ncurr-f 可变值不是类似于命令式语言中的循环变量吗?

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

没有。您始终可以将 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 覆盖唯一的堆栈帧而不是分配一个新的堆栈帧。这仅在调用者的堆栈帧此后不再被引用时有效——我们称之为 tail position

loop 是语法糖。我怀疑它是一个宏,但它可能是。除了较早的绑定应该对较晚的绑定可用,如 let,尽管我认为这个问题目前没有实际意义。

正如 Thumbnail 指出的那样,在 clojure 中使用 loop-recur 具有与经典递归函数调用相同的形式和效果。它存在的唯一原因是它比纯递归更有效。

由于 recur 只能出现在尾部位置,因此可以保证 loop "variables" 将不再需要。因此,您不需要将它们保存在堆栈中,因此不使用堆栈(不同于嵌套函数调用,无论是否递归)。最终结果是它的外观和行为与其他语言中的命令式循环非常相似。

与 Java 式 for 循环相比的改进是所有 "variables" 仅限于 "changing" loop 表达式中初始化时和在 recur 表达式中更新时。循环体和其他任何地方都不能对变量进行任何更改(例如可能会改变 Java 中的循环变量的嵌入式函数调用)。

由于对 "loop vars" 的位置有这些限制 mutated/updated,错误无意更改它们的机会减少了。限制的代价是循环不如传统的 Java 式循环灵活。

与任何事情一样,由您决定何时这种成本效益权衡比其他可用的成本效益权衡更好。如果你想要一个纯 Java 风格的循环,很容易使用 clojure 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

即使在我们使用原子来模拟 Java 中的可变变量的最后一种情况下,至少我们改变某些东西的每个地方都明显地用 swap! 函数标记,这使得它更难错过 "accidental" 突变。

P.S。如果你想使用 spyx 它是 in the Tupelo library