Clojure:通过fn名称重复与递归

时间:2016-08-31 15:45:06

标签: recursion clojure

我只是Clojure的初学者,我一直在尝试4clojure.com问题。在那里我偶然发现了一个问题,我应该写一个flatten实现。

基本上理解尾调用优化的概念,以及recur如何允许不消耗堆栈,而不是“正常”递归(我不知道是否有正确的)术语)。

这就是为什么我不知道这里发生了什么:

(defn foo1 [x]
  (if (> x 0)
    (do (println x)
        (foo1 (dec x)))))

(defn foo2 [x]
  (if (> x 0)
    (do (println x)
        (recur (dec x)))))

正如预期的那样,foo1foo2在功能上是相同的,但是,如果参数足够大(在我的情况下为100000),我会在foo1上获得堆栈溢出™,而{ {1}}正常完成。

现在,问到foo2问题:

flatten

测试用例:

(defn flatten1 [ls]
  (mapcat
    #(if (coll? %)
      (flatten1 %)
      (list %))
    ls))

(defn flatten2 [ls]
  (mapcat
    #(if (coll? %)
      (recur %)
      (list %))
    ls))

预期结果:(flatten [1 [2] 3 [4 [5 6 [7] 8]]]) (flatten1 [1 [2] 3 [4 [5 6 [7] 8]]]) (flatten2 [1 [2] 3 [4 [5 6 [7] 8]]])

好吧,'(1 2 3 4 5 6 7 8)工作正常(无论如何都是一个小输入)。但是flatten1只是无限期地挂起。 flatten2不会针对recur设置的递归点进行定位吗?通过名称递归函数有什么区别(优化除外)?

1 个答案:

答案 0 :(得分:4)

通过稍微修改程序,您可以看到问题:

(ns clj.core
  (:require [tupelo.core :as t] )
  (:gen-class))
(t/refer-tupelo)

(defn flatten1 [ls]
  (mapcat
    (fn [it]
      (println "f1: it=" it)
      (if (coll? it)
        (flatten1 it)
        (list it)))
    ls))

(defn flatten2 [ls]
  (mapcat
    (fn [it]
      (println "f2: it=" it)
      (if (coll? it)
        (recur it)
        (list it)))
    ls))

(defn -main
  [& args]
  (newline) (println "main - 1")
  (spyx (flatten  [1 [2] 3 [4 [5 6 [7] 8]]]))

  (newline) (println "main - 2")
  (spyx (flatten1 [1 [2] 3 [4 [5 6 [7] 8]]]))

  (newline) (println "main - 3")
  (flatten2 [1 [2] 3 [4 [5 6 [7] 8]]])

运行代码会产生以下输出:

main - 1
(flatten [1 [2] 3 [4 [5 6 [7] 8]]]) => (1 2 3 4 5 6 7 8)

main - 2
f1: it= 1
f1: it= [2]
f1: it= 2
f1: it= 3
f1: it= [4 [5 6 [7] 8]]
f1: it= 4
f1: it= [5 6 [7] 8]
f1: it= 5
f1: it= 6
f1: it= [7]
f1: it= 7
f1: it= 8
(flatten1 [1 [2] 3 [4 [5 6 [7] 8]]]) => (1 2 3 4 5 6 7 8)

main - 3
f2: it= 1
f2: it= [2]
f2: it= [2]
f2: it= [2]
f2: it= [2]
f2: it= [2]
f2: it= [2]
f2: it= [2]
f2: it= [2]

所以你可以看到它卡在[2]项上,输入列表的第二个元素。

失败的原因是recur语句只跳回到最里面的函数,即原始问题中的匿名表单#(if ...),第2个表单中的(fn [it] ...)形式版。

请注意recur只能“跳转”到最里面的fn / loop目标。您无法使用recur跳出内部匿名函数以达到flatten2。由于它只跳转到内部函数,因此1-elem集合[2]不会替换ls调用结束时的mapcat值,因此您将获得无限循环。 / p>

任何编程的最佳建议是“保持简单”。对于大多数问题,递归比loop / recur更简单。

在JVM上,每个堆栈帧都需要一些内存(请参阅有关-Xs开关的文档以增加)。如果使用太多堆栈帧,最终将耗尽内存(由-Xmx开关控制)。您通常应该能够依靠至少1000个堆栈帧(您可以测试您是否喜欢您的机器和参数)。因此,根据经验,如果递归深度为1000或更小,请不要担心使用loop/recur