Search code examples
clojuremacros

Using let style destructuring for def


Is there a reasonable way to have multiple def statements happen with destructing the same way that let does it? For Example:

(let [[rtgs pcts] (->> (sort-by second row)
                       (apply map vector))]
 .....)

What I want is something like:

(defs [rtgs pcts] (->> (sort-by second row)
                       (apply map vector)))

This comes up a lot in the REPL, notebooks and when debugging. Seriously feels like a missing feature so I'd like guidance on one of:

  • This exists already and I'm missing it
  • This is a bad idea because... (variable capture?, un-idiomatic?, Rich said so?)
  • It's just un-needed and I must be suffering from withdrawals from an evil language. (same as: don't mess up our language with your macros)

A super short experiment give me something like:

(defmacro def2 [[name1 name2] form] 
  `(let [[ret1# ret2#] ~form]
      (do (def ~name1 ret1#)
      (def ~name2 ret2#))))

And this works as in:

(def2 [three five] ((juxt dec inc) 4))
three ;; => 3
five ;; => 5

Of course and "industrial strength" version of that macro might be:

  • checking that number of names matches the number of inputs. (return from form)
  • recursive call to handle more names (can I do that in a macro like this?)

Solution

  • While I agree with Josh that you probably shouldn't have this running in production, I don't see any harm in having it as a convenience at the repl (in fact I think I'll copy this into my debug-repl kitchen-sink library).

    I enjoy writing macros (although they're usually not needed) so I whipped up an implementation. It accepts any binding form, like in let.

    (I wrote this specs-first, but if you're on clojure < 1.9.0-alpha17, you can just remove the spec stuff and it'll work the same.)

    (ns macro-fun
      (:require
       [clojure.spec.alpha :as s]
       [clojure.core.specs.alpha :as core-specs]))
    
    (s/fdef syms-in-binding
      :args (s/cat :b ::core-specs/binding-form)
      :ret (s/coll-of simple-symbol? :kind vector?))
    
    (defn syms-in-binding
      "Returns a vector of all symbols in a binding form."
      [b]
      (letfn [(step [acc coll]
                (reduce (fn [acc x]
                          (cond (coll? x) (step acc x)
                                (symbol? x) (conj acc x)
                                :else acc))
                        acc, coll))]
        (if (symbol? b) [b] (step [] b))))
    
    (s/fdef defs
      :args (s/cat :binding ::core-specs/binding-form, :body any?))
    
    (defmacro defs
      "Like def, but can take a binding form instead of a symbol to
       destructure the results of the body.
       Doesn't support docstrings or other metadata."
      [binding body]
      `(let [~binding ~body]
         ~@(for [sym (syms-in-binding binding)]
             `(def ~sym ~sym))))
    
    
    ;; Usage
    
    (defs {:keys [foo bar]} {:foo 42 :bar 36})
    
    foo ;=> 42
    
    bar ;=> 36
    
    (defs [a b [c d]] [1 2 [3 4]])
    
    [a b c d] ;=> [1 2 3 4]
    
    (defs baz 42)
    
    baz ;=> 42
    

    About your REPL-driven development comment:

    I don't have any experience with Ipython, but I'll give a brief explanation of my REPL workflow and you can maybe comment about any comparisons/contrasts with Ipython.

    I never use my repl like a terminal, inputting a command and waiting for a reply. My editor supports (emacs, but any clojure editor should do) putting the cursor at the end of any s-expression and sending that to the repl, "printing" the result after the cursor.

    I usually have a comment block in the file where I start working, just typing whatever and evaluating it. Then, when I'm reasonably happy with a result, I pull it out of the "repl-area" and into the "real-code".

    (ns stuff.core)
    
    ;; Real code is here.
    ;; I make sure that this part always basically works,
    ;; ie. doesn't blow up when I evaluate the whole file
    
    (defn foo-fn [x]
      ,,,)
    
    (comment
    
      ;; Random experiments.
    
      ;; I usually delete this when I'm done with a coding session,
      ;; but I copy some forms into tests.
    
      ;; Sometimes I leave it for posterity though,
      ;; if I think it explains something well.
    
      (def some-data [,,,])
    
      ;; Trying out foo-fn, maybe copy this into a test when I'm done.
      (foo-fn some-data)
    
      ;; Half-finished other stuff.
      (defn bar-fn [x] ,,,)
    
      (keys 42) ; I wonder what happens if...
    
      )
    

    You can see an example of this in the clojure core source code.