Loops Clojure-循环变量-不变性

Loops Clojure-循环变量-不变性,loops,clojure,immutability,Loops,Clojure,Immutability,我正在努力学习Clojure中的函数式编程。许多函数式编程教程都是从不变性的好处开始的,一个常见的例子是命令式语言中的循环变量。在这方面,Clojure的循环与它们有什么不同?例如: (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可变值类似于命令式语言

我正在努力学习Clojure中的函数式编程。许多函数式编程教程都是从不变性的好处开始的,一个常见的例子是命令式语言中的循环变量。在这方面,Clojure的
循环与它们有什么不同?例如:

(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
可变值类似于命令式语言中的循环变量

是不是
curr-n
curr-f
可变值与中的循环变量类似 命令式语言

不可以。您始终可以将
循环
-
递归
重写为递归函数调用。例如,您的
阶乘
函数可以重写

(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
将覆盖唯一的堆栈帧,而不是分配新的堆栈帧。这仅在调用方的堆栈帧此后从未被引用的情况下有效——我们称之为尾部位置


循环
是语法上的糖类。我怀疑这是一个宏观的,但它可能是。除了早期的绑定应该对后期的绑定可用之外,如在
let
中,尽管我认为这个问题目前还没有解决。

正如缩略图所指出的,在clojure中使用
循环递归
与经典的递归函数调用具有相同的形式和效果。它存在的唯一原因是它比纯递归有效得多

由于
循环
只能发生在尾部位置,因此可以保证不再需要
循环
“变量”。因此,您不需要在堆栈上保留它们,因此不使用堆栈(与嵌套函数调用不同,递归或非递归)。最终的结果是,它的外观和行为与其他语言中的命令式循环非常相似

与Java风格的
for
循环相比,所有“变量”仅限于在
循环
表达式中初始化和在
递归
表达式中更新时“更改”。在循环体中或其他任何地方都不能对变量进行任何更改(例如嵌入函数调用,它可能会在Java中改变循环变量)

由于对“循环变量”可以在何处变异/更新的限制,bug无意中更改它们的机会减少了。限制的代价是,循环不如传统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
;让clojure算出数字列表并累积结果
(定义事实范围[n]
(适用*(范围1(包括n)))
(spyx(事实范围4))
; 经典递归使用堆栈来计算
; 数字&累加中间结果
(定义事实重现[n]
(如果(<1 n)
(*n(事实重现(dec n)))
1))
(spyx(事实重现4))
; 让clojure算出数字列表;我们积累结果
(定义事实doseq[n]
(让[结果(原子1)]
(doseq[i(范围1(包括n))]
(交换!结果*i))
@结果)
(spyx(事实档案4))
; 我们算出一系列数字,并将结果累加起来
(定义事实可变[n]
(let[结果(原子1)
碳纳米管(原子1)]
(24)
(事实重现4)=>24
(事实doseq 4)=>24
(事实可变4)=>24
即使在最后一种情况下,我们使用原子来模拟Java中的可变变量,至少在每个地方我们都会使用
swap!
函数明显地标记一些东西,这使得我们很难错过“意外”变异


另外,如果你想使用
spyx
它是

如果我没记错的话,它们实际上在生成的字节码中发生了变异,因此它会编译成一个实际的循环。如果你把它粘贴到你的答案中,我会完全投它的票。从心智模型的角度来看,把
recur
看作是将名称重新绑定到新值——这些值仍然是不可变的e、 如果您将其中一个值存储在某个位置(在atom中,&c),它不会因为
recur
恰好重新绑定了原始值而改变。