我认为这里有几件事要学习。
首先,一种通用规则-递归函数通常具有自然顺序,而添加累加器则相反。你可以看到,因为当一个“正常”(没有累加器)递归函数运行时,它会做一些工作来计算一个值,然后递归生成列表的尾部,最后以一个空列表结束。相比之下,使用累加器时,您从空列表开始并在前面添加东西 - 它在另一个方向增长。
所以通常,当你添加一个累加器时,你会得到一个相反的顺序。
现在通常这无关紧要。例如,如果您生成的不是一个序列,而是一个重复应用交换运算符(如加法或乘法)的值。那么无论哪种方式,您都会得到相同的答案。
但在你的情况下,这很重要。您将向后获取列表:
(defn my-range-0 [lo hi] ; normal recursive solution
(if (= lo hi)
nil
(cons lo (my-range-0 (inc lo) hi))))
(deftest test-my-range-1
(is (= '(0 1 2) (my-range-0 0 3))))
(defn my-range-1 ; with an accumulator
([lo hi] (my-range-1 lo hi nil))
([lo hi acc]
(if (= lo hi)
acc
(recur (inc lo) hi (cons lo acc)))))
(deftest test-my-range-1
(is (= '(2 1 0) (my-range-1 0 3)))) ; oops! backwards!
通常,您可以做的最好的解决方法就是在最后反转该列表。
但这里有另一种选择——我们实际上可以倒着做。而不是增加下限,您可以减少上限:
(defn my-range-2
([lo hi] (my-range-2 lo hi nil))
([lo hi acc]
(if (= lo hi)
acc
(let [hi (dec hi)]
(recur lo hi (cons hi acc))))))
(deftest test-my-range-2
(is (= '(0 1 2) (my-range-2 0 3)))) ; back to the original order
[注意-下面还有另一种反转方式;我没有很好地构建我的论点]
其次,正如您在my-range-1
和中看到的那样my-range-2
,使用累加器编写函数的一种好方法是作为具有两组不同参数的函数。这为您提供了一个非常干净(恕我直言)的实现,而无需嵌套函数。
你还有一些关于序列的更一般conj
的问题,等等。这里的clojure有点乱,但也很有用。上面我一直在用基于缺点的列表给出一个非常传统的观点。但 clojure 鼓励您使用其他序列。与 cons 列表不同,向量向右而不是向左增长。所以另一种扭转该结果的方法是使用向量:
(defn my-range-3 ; this looks like my-range-1
([lo hi] (my-range-3 lo hi []))
([lo hi acc]
(if (= lo hi)
acc
(recur (inc lo) hi (conj acc lo)))))
(deftest test-my-range-3 ; except that it works right!
(is (= [0 1 2] (my-range-3 0 3))))
这里conj
是添加到右边。我没用conj
in my-range-1
,所以这里重写一下更清楚:
(defn my-range-4 ; my-range-1 written using conj instead of cons
([lo hi] (my-range-4 lo hi nil))
([lo hi acc]
(if (= lo hi)
acc
(recur (inc lo) hi (conj acc lo)))))
(deftest test-my-range-4
(is (= '(2 1 0) (my-range-4 0 3))))
请注意,这段代码看起来非常相似,my-range-3
但结果是向后的,因为我们从一个空列表开始,而不是一个空向量。在这两种情况下,都conj
在“自然”位置添加新元素。对于右侧的向量,但对于左侧的列表。
我突然想到你可能并不真正理解什么是列表。基本上 acons
创建一个包含两个东西(它的参数)的盒子。第一个是内容,第二个是列表的其余部分。所以列表(1 2 3)
基本上是(cons 1 (cons 2 (cons 3 nil)))
。相比之下,向量[1 2 3]
更像一个数组(尽管我认为它是使用树实现的)。
所以conj
有点令人困惑,因为它的工作方式取决于第一个参数。对于列表,它会调用cons
并在左侧添加内容。但是对于向量,它将数组(类似事物)扩展到右侧。另外,请注意,conj
将现有序列作为第一个参数,将要添加的东西作为第二个参数,而cons
反过来(要添加的东西首先出现)。
以上所有代码可在https://github.com/andrewcooke/clojure-lab获得
update: i rewrote the tests so that the expected result is a quoted list in the cases where the code generates a list. =
will compare lists and vectors and return true if the content is the same, but making it explicit shows more clearly what you're actually getting in each case. note that '(0 1 2)
with a '
in front is just like (list 0 1 2)
- the '
stops the list from being evaluated (without it, 0
would be treated as a command).