【问题标题】:Cross between "dotimes" and "for" functionality?在“dotimes”和“for”功能之间交叉?
【发布时间】:2011-05-31 04:45:43
【问题描述】:

我经常发现自己希望使用整数索引多次有效地运行 Clojure 函数(如“dotimes”),但也希望将结果作为现成的序列/列表(如“for”)输出。

即我想做这样的事情:

(fortimes [i 10] (* i i))

=> (0 1 4 9 16 25 36 49 64 81)

显然可以这样做:

(for [i (range 10)] (* i i))

但我想尽可能避免创建和丢弃临时范围列表。

在 Clojure 中实现这一目标的最佳方法是什么?

【问题讨论】:

  • 这个问题的最新消息是什么? clj-iterate 是最好的解决方案还是有更好的选择?

标签: loops macros clojure


【解决方案1】:

如您在第二个示例中所示,在 for 循环中生成范围是在 Clojure 中解决此问题的惯用解决方案。

由于 Clojure 以函数式范式为基础,因此在 Clojure 中编程,默认情况下会生成类似这样的临时数据结构。但是,由于“range”和“for”命令都使用惰性序列进行操作,因此编写此代码不会强制整个临时范围数据结构立即存在于内存中。如果使用得当,本例中使用的惰性序列的内存开销非常低。此外,您的示例的计算开销是适度的,并且应该只随着范围的大小线性增长。对于典型的 Clojure 代码,这被认为是可接受的开销。

如果临时范围列表对于您的情况绝对、肯定不可接受,那么完全避免这种开销的适当方法是使用原子或瞬态编写代码:http://clojure.org/transients。但是,如果您这样做,您将放弃 Clojure 编程模型的许多优点,以换取稍微更好的性能。

【讨论】:

    【解决方案2】:

    我已经编写了一个迭代宏,它可以非常有效地完成这个和其他类型的迭代。该软件包在 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)
    

    这不会创建一个临时列表。

    【讨论】:

    • +1 一个不错的小工具!作为一个有趣的问题,它如何设法避免临时列表?
    【解决方案3】:

    我不确定您为什么关心“创建和丢弃”range 函数创建的惰性序列。 dotimes 完成的有界迭代可能更有效,它是一个内联增量并与每个步骤进行比较,但您可能需要支付额外的费用来表达您自己的列表连接。

    典型的 Lisp 解决方案是将新元素添加到您构建的列表中,然后破坏性地反转该构建列表以产生返回值。其他允许在恒定时间内附加到列表的技术是众所周知的,但它们并不总是被证明比 prepend-then-reverse 方法更有效。

    在 Clojure 中,您可以使用 transients 到达那里,依赖于 conj! 函数的破坏性行为:

    (let [r (transient [])]
      (dotimes [i 10]
        (conj! r (* i i))) ;; destructive
      (persistent! r))
    

    这似乎可行,但the documentation on transients 警告说,不应使用conj! 来“对值进行适当的抨击”——也就是说,依靠破坏性行为来代替捕获返回值。因此,需要重写该表单。

    为了将上面的r 重新绑定到每次调用conj! 所产生的新值,我们需要使用atom 来引入更多的间接级别。不过,那时我们只是在与dotimes 作斗争,最好使用looprecur 编写自己的表单。

    如果能够将向量预分配为与迭代边界相同的大小,那就太好了。我看不出有什么办法。

    【讨论】:

    • 避免创建和丢弃额外序列的主要原因是尽量减少垃圾。是的,我知道现在垃圾收集真的很便宜,但它确实会导致延迟/GC 暂停问题,这在某些应用程序中是一个真正的问题
    【解决方案4】:
    (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)
    

    【讨论】:

    • 谢谢约翰 :-)!这是一个非常优雅的小解决方案。虽然它在做相反的事情之前仍然会创建一个不必要的临时列表,对吧?有什么办法可以避免吗?我认为以相反的顺序执行代码实际上可能有意义,尽管这可能会与任何副作用混淆......
    【解决方案5】:

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

    它基本上是一个列表,您可以在上面连接到最后。

    【讨论】:

      猜你喜欢
      • 2022-01-08
      • 2019-10-02
      • 1970-01-01
      • 2011-02-25
      • 1970-01-01
      • 2019-06-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多