“dotimes”和“for”功能之间的交叉?

时间:2010-12-27 15:34:10

标签: loops macros clojure

我经常发现自己想要使用整数索引(如“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中实现这一目标的最佳方法是什么?

5 个答案:

答案 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作斗争,最好使用looprecur编写自己的表单。

能够预先将矢量预先分配为与迭代绑定大小相同,这将是很好的。我没有办法这样做。

答案 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)

嗯,似乎无法回答你的评论,因为我没有注册。但是,clj-iterate使用PersistentQueue,它是运行时库的一部分,但不通过阅读器公开。

它基本上是一个你可以结束的列表。