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 ~@bind-list
                     :or (hash-map ~sym-arg-bindings))
           (vec ~argv)]
       ~@body)))

(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 "clojure.spec.alpha$regex_spec_impl$reify__2509@22ffdc30"],
: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.

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]
       ~@body)))

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)]
   ~@body)))