Clojure递归与命令式循环

clojure recur vs imperative loop

学习Clojure并尝试了解实现:

与以下内容有何区别:

1
2
3
4
5
6
7
8
9
(def factorial
  (fn [n]
    (loop [cnt n acc 1]
       (if (zero? cnt)
            acc
          (recur (dec cnt) (* acc cnt))
; in loop cnt will take the value (dec cnt)
; and acc will take the value (* acc cnt)
))))

和以下类似C的伪代码

1
2
3
4
5
6
7
8
function factorial (n)
    for( cnt = n,  acc = 1) {
        if (cnt==0) return acc;
        cnt = cnt-1;
        acc = acc*cnt;
    }
// in loop cnt will take the value (dec cnt)
// and acc will take the value (* acc cnt)

clojure的" loop "和" recur "是专门设计用来编写简单命令式循环的形式吗?
(假设伪代码的" for "创建了它自己的作用域,因此cnt和acc仅存在于循环中)


Clojure的looprecur形式是否专门设计用于编写简单的命令式循环?

是的。

在功能方面:

  • 循环是递归的简并形式,称为尾递归。
  • 循环主体中的"变量"未修改。代替,
    每当重新进入循环时,它们就会重新被训练。

Clojure \\的recur对周围的递归点进行尾递归调用。

  • 它重用了一个堆栈框架,因此工作更快,避免了堆栈
    溢出。
  • 它只能在任何通话中作为最后的动作发生-在所谓的尾巴位置。

每个连续的recur调用将覆盖最后一个,而不是将其堆积。

递归点是

  • fn形式,可能以defnletfn伪装或
  • loop形式,它也可以绑定/设置/初始化
    当地人/变量。

因此您的factorial函数可以被重写

1
2
3
4
5
6
7
(def factorial
  (fn [n]
    ((fn fact [cnt acc]
      (if (zero? cnt)
        acc
        (fact (dec cnt) (* acc cnt))))
     n 1)))

...速度较慢,并且有堆栈溢出的风险。

并非每个C / C循环都能顺利转换。您可能会从嵌套循环中麻烦,在嵌套循环中,内部循环会修改外部循环中的变量。

顺便说一下,您的factorial函数

  • 将很快导致整数溢出。如果你想避免
    为此,使用1.0而不是1来获取浮点数(双精度)
    算术运算,或使用*'而不是*来获取Clojure \\的BigInt
    算术。
  • 会否定论点无休止地循环。

后者的快速解决方案是

1
2
3
4
5
6
7
(def factorial
  (fn [n]
    (loop [cnt n acc 1]
      (if (pos? cnt)
        (recur (dec cnt) (* acc cnt))
        acc))))
; 1

...尽管最好返回nilDouble.NEGATIVE_INFINITY


一种查看loop / recur的方法是,它可以让您编写具有功能性的代码,但是底层实现本质上最终是一个命令式循环。

要查看其功能,请以您的示例为例

1
2
3
4
5
6
(def factorial
  (fn [n]
    (loop [cnt n acc 1]
      (if (zero? cnt)
        acc
        (recur (dec cnt) (* acc cnt))))))

并重写它,以便将loop形式分解为单独的帮助函数:

1
2
3
4
5
6
7
8
9
(def factorial-helper
  (fn [cnt acc]
    (if (zero? cnt)
      acc
      (recur (dec cnt) (* acc cnt)))))

(def factorial'
  (fn [n]
    (factorial-helper n 1)))

现在您可以看到helper函数只是在调用自身;您可以将recur替换为函数名称:

1
2
3
4
5
(def factorial-helper
  (fn [cnt acc]
    (if (zero? cnt)
      acc
      (factorial-helper (dec cnt) (* acc cnt)))))

factorial-helper中使用时,您可以将recur看作是简单地进行递归调用,并通过基础实现对其进行了优化。

我认为一个重要的想法是,它允许基础实现成为命令性循环,但是您的Clojure代码仍然保持功能。换句话说,它不是允许您编写涉及任意分配的命令性循环的构造。但是,如果以这种方式构造功能代码,则可以获得与命令循环相关的性能优势。

一种将命令式循环成功转换为这种形式的方法是将命令式分配更改为递归调用的自变量参数"已分配"的表达式。但是,当然,如果遇到命令式循环进行任意分配,则可能无法将其转换为这种形式。在此视图中,loop / recur是受约束更严格的构造。