我只是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)))))
正如预期的那样,foo1
和foo2
在功能上是相同的,但是,如果参数足够大(在我的情况下为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
设置的递归点进行定位吗?通过名称递归函数有什么区别(优化除外)?
答案 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
。