5
votes

I do a lot of computation in let blocks, returning hash-maps containing the data. The following is a not-so-minimal example:

(def ground-truth
(let [n              201
      t1             2.0
      linspace       (fn [a b n] (let [d (/ (- b a) (dec n))]
                                   (map (fn [x] (+ a (* x d))) (range n))))
      times          (vec (linspace 0.0, t1, n))
      wavelength     1
      wavespeed      1
      f              (* (/ wavespeed wavelength) 2 Math/PI)
      dt             (- (get times 1) (get times 0))
      amplitude      5.0
      ground-level   10.0
      h-true         (mapv #(+ ground-level 
                               (* amplitude (Math/sin (* f %))))
                           times)
      h-dot-true     (mapv #(* amplitude f (Math/cos (* f %)))
                           times)
      baro-bias-true -3.777]
    {:n n, :times times, :f f, :dt dt, :h-true h-true,
     :h-dot-true h-dot-true, :baro-bias-true baro-bias-true}))

What I want to do is get rid of the repetition in the final expression. It's not such a big problem for this little example, but I have some that are much longer and more elaborate and the repetition makes modifying the expression tedious and error-prone.

I tried this macro:

(defmacro hashup [name-list]
`(into {}
        (map vector
             (mapv keyword ~name-list)
             (mapv eval    ~name-list))))

which works only if eval works, which works on vars:

(def foo 41) (def bar 42)
(hashup '[foo bar])

{:foo 41, :bar 42}

but not on let blocks:

(let [a 1, b (inc a)] (hashup '[a b]))

CompilerException java.lang.RuntimeException: Unable to resolve symbol: a in this context, compiling:(null:1:1) Util.java: 221 clojure.lang.Util/runtimeException
core.clj: 3105 clojure.core$eval/invokeStatic

as expected after reviewing the following SO questions, amongst many others: Variable scope + eval in Clojure, eval a list into a let on clojure

One might say "well, you can either have repetition outside your let blocks by deffing your variables in namespaces and then using something like hashup, or you can have your repetition at the bottom of your let blocks and forget about macro magic. But there is no way to Don't Repeat Yourself in this exact use case.

Did I miss a good way to DRY out this kind of code?

3
Not an answer, but you might want to have a look at fnk from github.com/plumatic/plumbingcfrick
Just a little advice in addition to the fine answer by Wout Neirynck: the problem with your own attempt at a macro is that you're treating it too much like a regular function, i.e. you're trying to create the vector directly. A macro should not try to create the final result, but rather create an expression that will create the final result when it gets evaluated. Since you're creating an expression that will then get evaluated, you don't need to use eval explicitly.Rörd
@Rörd from your explanation, I think I now have a much better understanding of when evaluation happens with macros: we get a rewrite of code first with no evaluation, then evaluation later at run time. That's consistent with Wout's answer below, in which vars is never evaluated. The reflex of prefixing every macro with a backtick to suspend evaluation induces a distraction: the evaluation it's suspending is rewrite-time evaluation, which should not be conflated with run-time evaluation.Reb.Cabin

3 Answers

9
votes

Try this:

(defmacro ->hash [& vars]
  (list `zipmap
    (mapv keyword vars)
    (vec vars)))

Then:

(->hash a b c) => {:a a :b b :c c}

And it also works inside let blocks.

2
votes

You can do what flatland.useful.map/keyed does: generate the map structure at compile time, instead of making the caller generate a vector of keys, and a vector of values, and then call zipmap on them. A simpler version of the same thing, if you don't care about being able to build maps like this keyed by string or by symbol, would be:

(defmacro keyed [ks]
  (into {}
    (for [k ks]
      [(keyword k) k])))

(let [x 1, y 2]
  (keyed [x y]))

; {:x 1, :y 2}

Note that useful chooses to wrap things in an "unnecessary" vector here for symmetry with the {:keys [x y]} destructuring form, and as mentioned also provides :strs and :syms analogues.

2
votes

Try that one:

(defmacro letm
  [bindings]
  `(let* ~(destructure bindings)
     (merge ~@(map #(hash-map (keyword %) %) (take-nth 2 bindings)))))

(letm [a 1
       b (+ 1 2)
       c (println "As")
       d (+ b 2)
       e 'selam])

=> As
=> {:a 1, :b 3, :c nil, :d 5, :e selam}