Issue creating macro that emits a let with associative destructuring for the binding form

Every now and then I try to build my macro muscle and every now and then need a spotter :). In my case i’m looking to build a macro that when given a seq of args (say via a -main function) create a let form that uses the associative property of vectors to bind to named values for easier use within the body form. So for example here is expected input and output

;; input
(with-args args
  [hostname "127.0.0.1"
   port "3030"]
  [hostname port])

;; output
(let [{hostname 0
       port 1
       :or {hostname "127.0.0.1"
            port "3030"}} (vec args)]
  [hostname port])

Here you can see that you provide the actual args as a seq, and the bindings with defaults, the output is the associative destructuring on indexes with a default using :or. This is the macro i’ve come up with.

 (defmacro with-args
  {:style/indent 1}
  [argv arg-bindings & body]
  (let [sym-arg-bindings (map #(if (symbol? %1) (gensym %1) %1)
                              arg-bindings) 
        bind-list (->> (partition 2 sym-arg-bindings)
                       (map-indexed (fn [i v]
                                      [(first v) i]))
                       flatten)]
    `(let [(hash-map [email protected]
                     :or (hash-map ~sym-arg-bindings))
           (vec ~argv)]
       [email protected])))

(println 
  (macroexpand '(with-args ["127.0.0.1 3030"]
                  [hostname "127.0.0.1"
                   port "6060"]
                  (println hostname)
                  (println port))))

Running the macroexpand gives the following issue stack trace. I think I offended the compile from the looks of it, but can’t exactly wrap my head around the issue. From what I guess it is expecting the [symbol value] form of the let binding instead of a map.

Call to clojure.core/let did not conform to spec.
#:clojure.spec.alpha{:problems ({:path [:bindings :form :local-symbol],
				       :pred clojure.core/simple-symbol?,
				       :val (clojure.core/hash-map hostname17622 0 port17623 1 :or (clojure.core/hash-map (hostname17622 "127.0.0.1" port17623 "6060"))),
				       :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/local-name],
				       :in [0 0]} {:path [:bindings :form :seq-destructure],
				       :pred clojure.core/vector?,
				       :val (clojure.core/hash-map hostname17622 0 port17623 1 :or (clojure.core/hash-map (hostname17622 "127.0.0.1" port17623 "6060"))),
				       :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/seq-binding-form],
				       :in [0 0]} {:path [:bindings :form :map-destructure],
				       :pred clojure.core/map?,
				       :val (clojure.core/hash-map hostname17622 0 port17623 1 :or (clojure.core/hash-map (hostname17622 "127.0.0.1" port17623 "6060"))),
				       :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings],
				       :in [0 0]} {:path [:bindings :form :map-destructure],
				       :pred clojure.core/map?,
				       :val (clojure.core/hash-map hostname17622 0 port17623 1 :or (clojure.core/hash-map (hostname17622 "127.0.0.1" port17623 "6060"))),
				       :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-special-binding],
				       :in [0 0]}),
:spec #object[clojure.spec.alpha$regex_spec_impl$reify__2509 0x22ffdc30 "[email protected]"],
:value ([(clojure.core/hash-map hostname17622 0 port17623 1 :or (clojure.core/hash-map (hostname17622 "127.0.0.1" port17623 "6060"))) (clojure.core/vec ["127.0.0.1 3030"])] (println hostname) (println port)),
:args ([(clojure.core/hash-map hostname17622 0 port17623 1 :or (clojure.core/hash-map (hostname17622 "127.0.0.1" port17623 "6060"))) (clojure.core/vec ["127.0.0.1 3030"])] (println hostname) (println port))}

The problem is that it is expanding to a call to hash-map instead of an actual hash map – so you need to lift that call (and vec) into the compile time portion of the macro.

1 Like

That did it. Removed the gensyms, didn’t think about the usage of them in the body expression

(defmacro with-args
  {:style/indent 1}
  [argv arg-bindings & body]
  (let [bind-list (->> (partition 2 arg-bindings)
                       (map-indexed (fn [i v]
                                      [(first v) i]))
                       flatten)
        bind-form (-> (apply hash-map bind-list)
                      (assoc :or (apply hash-map arg-bindings)))
        bind-arg (vec argv)]
    `(let [~bind-form ~bind-arg]
       [email protected])))
1 Like

I have noticed that it doesn’t respond to a symbol, so if I ware to give it the form

(let [args ["127.0.0.1"]]
  (with-args args
    [host "0.0.0.0"
     port "3030"]
    (tap> [host port])))

it complains that args is a symbol not an Object. is this to do with the hygene detail in clojure macros?

exact error

   Unable to convert: class clojure.lang.Symbol to Object[]

I think I was wrong about lifting the vec call. This may work better:

`(let [~bind-form (vec ~argv)]
   [email protected])))