我经常发现自己想要使用整数索引(如“dotimes”)多次有效地运行Clojure函数,但也将结果作为现成的序列/列表(如“for”)得到。
即。我想做这样的事情:
(fortimes [i 10] (* i i))
=> (0 1 4 9 16 25 36 49 64 81)
显然可以这样做:
(for [i (range 10)] (* i i))
但是如果可能的话,我想避免创建和丢弃临时范围列表。
在Clojure中实现这一目标的最佳方法是什么?
答案 0 :(得分:6)
在第二个示例中显示的for循环中生成范围是在Clojure中解决此问题的惯用解决方案。
由于Clojure以功能范例为基础,因此默认情况下在Clojure中编程将生成这样的临时数据结构。但是,由于“range”和“for”命令都使用惰性序列,因此编写此代码不会强制整个临时范围数据结构立即存在于内存中。如果使用得当,则本例中使用的延迟seq的内存开销非常低。此外,您的示例的计算开销是适度的,并且应该只与范围的大小线性增长。对于典型的Clojure代码,这被认为是可接受的开销。
完全避免这种开销的适当方法,如果临时范围列表对于您的情况绝对是不可接受的,那就是使用原子或瞬态来编写代码:http://clojure.org/transients。但是,如果你这样做,你将放弃Clojure编程模型的许多优点,以换取更好的性能。
答案 1 :(得分:5)
我编写了一个迭代宏,可以非常高效地完成此操作和其他类型的迭代。该包在github和clojars上都被称为clj-iterate。例如:
user> (iter {for i from 0 to 10} {collect (* i i)})
(0 1 4 9 16 25 36 49 64 81 100)
这不会创建临时列表。
答案 2 :(得分:3)
我不确定你为什么要关注“创建和丢弃”由range
函数创建的延迟序列。由dotimes
完成的有界迭代可能更有效,它是内联增量并与每个步骤进行比较,但您可能需要支付额外的费用来表达您自己的列表连接。
典型的Lisp解决方案是将新元素添加到您构建的列表中,然后破坏性地反转该构建列表以产生返回值。允许在恒定时间内附加到列表的其他技术是众所周知的,但它们并不总是比 prepend-then-reverse 方法更有效。
在Clojure中,您可以使用瞬态来实现目标,依赖conj!
函数的破坏性行为:
(let [r (transient [])]
(dotimes [i 10]
(conj! r (* i i))) ;; destructive
(persistent! r))
这似乎有效,但是the documentation on transients警告说,不应该使用conj!
来“压缩价值” - 也就是说,依靠破坏性行为代替捕获返回值。因此,需要重写该表格。
为了将上面的r
重新绑定到每次调用conj!
所产生的新值,我们需要使用atom来引入更多级别的间接。那时,我们只是在与dotimes
作斗争,最好使用loop
和recur
编写自己的表单。
能够预先将矢量预先分配为与迭代绑定大小相同,这将是很好的。我没有办法这样做。
答案 3 :(得分:3)
(defmacro fortimes [[i end] & code]
`(let [finish# ~end]
(loop [~i 0 results# '()]
(if (< ~i finish#)
(recur (inc ~i) (cons ~@code results#))
(reverse results#)))))
示例:
(fortimes [x 10] (* x x))
给出:
(0 1 4 9 16 25 36 49 64 81)
答案 4 :(得分:1)
它基本上是一个你可以结束的列表。