Clojure:使用“for”绑定构建集合

Clojure: building collections using `for` bindings

我对 clojure 还是很陌生,但我发现自己经常在其中使用的模式是这样的:我有一些集合,我想构建一个新集合,通常是哈希映射,out他们有一些过滤器或条件。总是有几种方法可以做到这一点:例如使用 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 性能的人能否告诉我这个实现是否有问题,whether/how我应该担心性能,假设我可能会非常频繁地使用这个宏来进行相对较大的操作集合?
  3. 我应该在上述宏的 reduce 版本上使用循环,反之亦然吗?
  4. 谁能看到更好的宏实现?

您的 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 非常适合基准测试,可以消除任何与性能相关的猜测。

我不想完全采用您的文档字符串示例代码,因为它不是惯用的 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}}