Search code examples
clojure

Clojure macro: Create local vars from a map


I have this sample code where I create vars by iterating over the key value pair of the map.

(defmacro block [bindings & body] 
  `(let [
    ~@(mapcat (fn [[k v]] [(if (symbol? k) k (symbol (name k))) `~v]) bindings) ]
  ~body))

(block {:a 1 :b 2} (if true (prn "true" a b) (prn "false")))

It works fine. Ouput: "true" 1 2


But now I want to pass the same map as a var but, it thows an exception.

IllegalArgumentException Don't know how to create ISeq from: clojure.lang.Symbol

(def ctx {:a 3 :b 4})

(block ctx (if true (prn "true" a b) (prn "false")))

Solution

  • The reason it doesn't work, when you pass a symbol that refers to a var that holds a map, is because the macro is only seeing the symbol literal — not the value it refers to. When you pass it a map literal, the macro is seeing the map literal.

    If you wanted it to also support symbols, you'll need to resolve the var the symbol refers to and get its value e.g. (var-get (resolve bindings)):

    (defmacro block [bindings & body]
      `(let [~@(mapcat (fn [[k v]] [(if (symbol? k)
                                      k
                                      (symbol (name k))) `~v])
                       (cond
                         (map? bindings) bindings
                         (symbol? bindings) (var-get (resolve bindings))
                         :else (throw (Exception. "bindings must be map or symbol"))))]
         ~@body))
    
    (block ctx (if true (prn "true" a b) (prn "false")))
    "true" 3 4
    (block {:a 3 :b 4} (if true (prn "true" a b) (prn "false")))
    "true" 3 4
    

    You also need to "splice" body into the form using ~@, because body will be a sequence of forms even if it only contains one form.

    It can also help to look at how your macro expands when troubleshooting it:

    (macroexpand-1 '(block ctx (if true (prn "true" a b) (prn "false"))))
    => (clojure.core/let [a 3 b 4] (if true (prn "true" a b) (prn "false")))