【问题标题】:Clojure: building collections using `for` bindingsClojure:使用“for”绑定构建集合
【发布时间】:2016-02-15 02:54:34
【问题描述】:

我对 clojure 还是很陌生,但是我发现自己经常在其中使用的一个模式是这样的:我有一些集合,我想用它们构建一个新集合,通常是一个哈希映射带有一些过滤器或条件。总是有几种方法可以做到这一点:例如使用loop 或使用reduce 结合map/filter,但我想实现更像for 宏的东西,它有很好的语法用于控制在循环中评估的内容。我想生成一个语法如下的宏:

(defmacro build
  "(build sym init-val [bindings...] expr) evaluates the given expression expr
   over the given bindings (treated identically to the bindings in a for macro); 
   the first time expr is evaluated the given symbol sym is bound to the init-val
   and every subsequent time to the previous expr. The return value is the result
   of the final expr. In essence, the build macro is to the reduce function
   as the for macro is to the map function.

   Example:
     (build m {} [x (range 4), y (range 4) :when (not= x y)]
       (assoc m x (conj (get m x #{}) y)))
      ;; ==> {0 #{1 3 2}, 1 #{0 3 2}, 2 #{0 1 3}, 3 #{0 1 2}}"
  [sym init-val [& bindings] expr]
  `(...))

查看 clojure.core 中的 for 代码,很明显我不想自己重新实现它的语法(甚至忽略复制代码的普通危险),而是想出类似 for 的行为在上面的宏中比我最初预期的要复杂得多。我最终想出了以下方法,但我觉得 (a) 这可能不是非常高效,并且 (b) 应该有更好的,仍然是 clojure-y 的方法来做到这一点:

(defmacro build
  [sym init-val bindings expr]
  `(loop [result# ~init-val, s# (seq (for ~bindings (fn [~sym] ~expr)))]
     (if s#
       (recur ((first s#) result#) (next s#))
       result#))
   ;; or `(reduce #(%2 %1) ~init-val (for ~bindings (fn [~sym] ~expr)))

我的具体问题:

  1. 是否有内置的 clojure 方法或库可以解决这个问题,或许更优雅?
  2. 假设我可能会非常频繁地将这个宏用于相对较大的集合,那么更熟悉 clojure 性能的人能否告诉我这个实现是否存在问题以及我是否应该/在多大程度上担心性能?李>
  3. 有什么好的理由我应该在上述宏的 reduce 版本上使用循环,反之亦然?
  4. 谁能看到更好的宏实现?

【问题讨论】:

    标签: loops for-loop clojure macros reduce


    【解决方案1】:

    我不想完全采用您的文档字符串的示例代码,因为它不是惯用的 clojure。但是取plumbing.core's for-map,你可以想出一个类似的for-map-update

    (defn update!
      "Like update but for transients."
      ([m k f] (assoc! m k (f (get m k))))
      ([m k f x1] (assoc! m k (f (get m k) x1)))
      ([m k f x1 x2] (assoc! m k (f (get m k) x1 x2)))
      ([m k f x1 x2 & xs] (assoc! m k (apply f (get m k) x1 x2 xs))))
    
    (defmacro for-map-update
      "Like 'for-map' for building maps but accepts a function as the value to build map values."
      ([seq-exprs key-expr val-expr]
       `(for-map-update ~(gensym "m") ~seq-exprs ~key-expr ~val-expr))
      ([m-sym seq-exprs key-expr val-expr]
       `(let [m-atom# (atom (transient {}))]
          (doseq ~seq-exprs
            (let [~m-sym @m-atom#]
              (reset! m-atom# (update! ~m-sym ~key-expr ~val-expr))))
          (persistent! @m-atom#))))
    
    (for-map-update
      [x (range 4)
       y (range 4)
       :when (not= x y)]
      x (fnil #(conj % y) #{} ))
    ;; => {0 #{1 3 2}, 1 #{0 3 2}, 2 #{0 1 3}, 3 #{0 1 2}}
    

    【讨论】:

    • 我不同意它是单调的。当然,它是一个自定义宏,但它与 Clojure 惯用语一点也不格格不入。事实上,语法和areduce很相似。
    • 感谢您提供 plumbing.core 链接---一直在寻找这样的东西。您能否指出“惯用 clojure”的定义或解释是什么使宏惯用?特别是考虑到您的版本需要更多代码并且仅支持我的一些功能(即,您的版本仅简化为地图并且不允许,例如,这种(相对常见)类型的情况:@987654326 @.)
    • 对我来说,m 就像循环中的可变变量。基本上是编写命令式代码而不是函数式代码。甚至瞬态都不会做的事情(您必须捕获返回值)。这是你在 Clojure 代码中很少看到的东西。但是是的,我同意,你的版本更强大。
    • 明白了,这很有意义。虽然,如果我理解你,我相信你是错误的 re:transients;以下工作正常,例如:(let [tr (transient {})] (assoc! tr :a 1) (persistent! tr)) -- 产生{:a 1}
    • @user21382 实际上这是行不通的——如果你 assoc! 有足够多的元素到你的瞬态图上,它的底层类型将会改变,assoc! 将返回一个不同的瞬态实例并且原来的将不包含您的新条目。参见例如stackoverflow.com/questions/29684803/…
    【解决方案2】:

    您的reduce 版本也是我基于问题陈述的第一个方法。我认为它很好而且很简单,我希望它能够很好地工作,特别是因为for 将生成一个分块序列,reduce 将能够非常快速地迭代。

    for 无论如何都会生成函数来生成输出,我不认为build 扩展引入的额外层会特别成问题。基于volatile! 对该版本进行基准测试可能仍然值得:

    (defmacro build [sym init-val bindings expr]
      `(let [box# (volatile! ~init-val)] ; AtomicReference would also work
         (doseq ~bindings
           (vreset! box# (let [~sym @box#] ~expr)))
         @box#))
    

    Criterium 非常适合进行基准测试,可以消除任何与性能相关的猜测。

    【讨论】:

    • 谢谢,这很有帮助;我会检查标准!
    • 仅供参考,我使用 Criterium 对构建的三个版本(循环、减少、易失)进行基准测试。我使用的代码是(let [R (doall (repeatedly 10000 rand))] (bench (build m #{} [r R] (conj m r))))。在我的桌面上,build 的 loop 和 reduce 版本的平均运行时间约为 3.8 ms,loop 版本在所有测试中都快得微不足道(大约 10 us)。 volatile 版本的运行时间约为 3.1 毫秒。
    • 干杯!我运行了一个类似的基准测试,使用向量而不是惰性序列作为输入——以便迭代分块序列——并且loop 版本显然更慢,volatile! 略快于reduce (8.68 ms / loop / reduce / volatile! 为 7.41 毫秒 / 7.27 毫秒。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-10
    • 1970-01-01
    • 1970-01-01
    • 2015-10-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多