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-n
和 curr-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
我正在尝试学习 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-n
和 curr-f
可变值不是类似于命令式语言中的循环变量吗?
Isn't
curr-n
andcurr-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