Question

I'm trying to write a macro which expands to a let form with destructuring. My problem is that I would like to have the list of the symbols that are defined in the let form, including those obtained by destruturing.

Use case

I'm trying to factor out this kind of behavior, for validation for example :

(let [a (foo bar)
      {x :x,
       y :y,
       {u :u, v: v :as nested-map} :nested} some-map]
  (and x y nested-map u v ; testing truthiness
       (valid-a? a)
       (valid-x? x)
       (valid-y? y)
       (valid-nested? nested-map)
       (valid-u-and-v? u v)
       ))

Proposed solution

It would be really nice to achieve this through some sort of and-let macro which I could call like this:

(and-let [a (foo bar)
          {x :x,
           y :y,
           {u :u, v: v :as nested-map} :nested} some-map]
         (valid-a? a)
         (valid-x? x)
         (valid-nested? nested-map)
         (valid-u-and-v? u v))

What I'm missing

But I'm missing some way of accessing the list of the symbols that are bound in the let form. If I had something like a list-bound-symbols function, I could do it like this :

(defmacro and-let
  "Expands to an AND close that previouly checks that the values declared in bindings are truthy, followed by the tests."
  [bindings & tests]
  (let [bound-symbols (list-bound-symbols bindings) ;; what I'm missing
        ]
    `(let ~bindings
       (and 
         ~@bound-symbols
         ~@tests)
     ))) 

Has anyone got a clue how I might do this?

Was it helpful?

Solution

Destructuring is handled by the clojure.core/destructure function. It's public, so we can call it ourselves and extract the names of all locals, including those naming intermediate results used in destructuring:

(defmacro and-let [bindings & tests]
  (let [destructured (destructure bindings)]
    `(let ~destructured
       (and ~@(take-nth 2 destructured)
            ~@tests))))

Seems to work:

(let [foo nil]
  (and-let [a 1
            [b c] [2 3]]
    (nil? foo)))
;= true

OTHER TIPS

You can do most of this with a function that deals with the map directly.

(defn validate [vm ; validation map
                dm ; data map
                ]
  (and
    (every? identity (map #(% dm) (flatten (keys vm))))
    (every? identity (map (fn [[k vf]]
                            (if (vector? k)
                              (apply vf (map #(% dm) k))
                              (vf (k dm))))
                          vm))))

For example

(validate {:s string?, :n number? :m number? [:m :n] > } {:s "Hello!", :m 5, :n 3})
; true

(validate {:s string?, :n number? :m number? [:m :n] > } {:s "Hello!", :m 5, :n 6})
; false

(validate {:s string?, :n number? :m number? :u \b [:m :n] > } {:s "Hello!", :m 5, :n 6})
; false

You can add any extraneous variables, a in your example, to the map beforehand. This would needlessly test for as truthiness. No harm done?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top