I'm still fairly new to clojure, but a pattern that I find myself using frequently in it goes something like this: I have some collections and I want to build a new collection, usually a hash-map, out of them with some filters or conditions. There are always a few ways to do this: using loop
or using reduce
combined with map
/filter
for example, but I would like to implement something more like the for
macro, which has great syntax for controlling what gets evaluated in the loop. I'd like to produce a macro with syntax that goes like this:
(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]
`(...))
Looking at the for
code in clojure.core, it's pretty clear that I don't want to re-implement its syntax myself (even ignoring the ordinary perils of duplicating code), but coming up with for-like behavior in the above macro is a lot trickier than I initially expected. I eventually came up with the following, but I feel that (a) this probably isn't terribly performant and (b) there ought to be a better, still clojure-y, way to do this:
(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)))
My specific questions:
- Is there a built-in clojure method or library that solves this already, perhaps more elegantly?
- Can someone who is more familiar with clojure performance give me an idea of whether this implementation is problematic and whether/how much I should be worried about performance, assuming that I may use this macro very frequently for relatively large collections?
- Is there any good reason that I should use the loop over the reduce version of the macro above, or vice versa?
- Can anyone see a better implementation of the macro?