Gastove
Gastove

Reputation: 916

Pass a data structure in to a macro for filling in

I'm trying to solve a problem: I need to create a map from passed-in values, but while the symbol names for the values are consistent, the keys they map to are not. For instance: I might be passed a value that is a user ID. In the code, I can always use the symbol user-id -- but depending on other factors, I might need to make a map {"userId" user-id} or {"user_id" user-id} or {:user-id user-id} or -- well, you get the picture.

I can write a macro that gets me part-way there:

(defmacro user1 [user-id] `{"userId" ~user-id}
(defmacro user2 [user-id] `{"user_id" ~user-id}

But what I'd much rather do is define a set of maps, then combine them with a given set of symbols:

(def user-id-map-1 `{"userId" `user-id}
(defn combiner [m user-id] m)            ;; <-- Around here, a miracle occurs.

I can't figure out how to get this evaluation to occur. It seems like I should be able to make a map containing un-evaluated symbols, then look up those symbols in the lexical scope of a function or macro that binds those symbols as locals -- but how?

Upvotes: 0

Views: 147

Answers (3)

Thumbnail
Thumbnail

Reputation: 13483

Instead of standardizing your symbolic names, use maps with standard keyword keys. You don't need to go near macros, and you can turn your maps into records if need be without much trouble.

What you know as

(def user1 {:id 3124, :surname "Adabolo", :forenames ["Julia" "Frances"]})

... can be transformed by mapping the keys with whatever function you choose:

(defn map-keys [keymap m]
  (zipmap (map keymap (keys m)) (vals m)))

For example,

(map-keys name user1)
;{"id" 3124, "surname" "Adabolo", "forenames" ["Julia" "Frances"]}

or

(map-keys {:id :user-id, :surname :family-name} user1)
;{:user-id 3124, :family-name "Adabolo", nil ["Julia" "Frances"]}

If you want rid of the nil entry, wrap the expression in (dissoc ... nil):

(defn map-keys [keymap m]
  (dissoc
    (zipmap (map keymap (keys m)) (vals m))
    nil))

Then

(map-keys {:id :user-id, :surname :family-name} user1)
;{:user-id 3124, :family-name "Adabolo"}

I see from Michał Marczyk's answer, which has priority, that the above essentially rewrites clojure.set/rename-keys, which, however ...

  • leaves missing keys untouched:

For example,

(clojure.set/rename-keys user1 {:id :user-id, :surname :family-name})
;{:user-id 3124, :forenames ["Julia" "Frances"], :family-name "Adabolo"}
  • doesn't work with normal functions:

For example,

(clojure.set/rename-keys user1 name)
;IllegalArgumentException Don't know how to create ISeq from: clojure.core$name ... 

If you forego the use of false and nil as keys, you can leave missing keys untouched and still use normal functions:

(defn map-keys [keymap m]
  (zipmap (map #(or (keymap %) %) (keys m)) (vals m)))

Then

(map-keys {:id :user-id, :surname :family-name} user1)
;{:user-id 3124, :family-name "Adabolo", :forenames ["Julia" "Frances"]}

Upvotes: 2

Michał Marczyk
Michał Marczyk

Reputation: 84351

How about putting your passed-in values in a map keyed by keywords forged from the formal parameter names:

(defmacro zipfn [map-name arglist & body]
  `(fn ~arglist
     (let [~map-name (zipmap ~(mapv keyword arglist) ~arglist)]
       ~@body)))

Example of use:

((zipfn argmap [x y z]
   argmap)
 1 2 3)
;= {:z 3, :y 2, :x 1}

Better yet, don't use macros:

;; could take varargs for ks (though it would then need another name)
(defn curried-zipmap [ks]
  #(zipmap ks %))

((curried-zipmap [:x :y :z]) [1 2 3])
;= {:z 3, :y 2, :x 1}

Then you could rekey this map using clojure.set/rename-keys:

(clojure.set/rename-keys {:z 3, :y 2, :x 1} {:z "z" :y "y" :x "x"})
;= {"x" 1, "z" 3, "y" 2}

The second map here is the "translation map" for the keys; you can construct in by merging maps like {:x "x"} describing how the individual keys ought to be renamed.

Upvotes: 1

Leon Grapenthin
Leon Grapenthin

Reputation: 9266

For the problem you described I can't find a reason to use macros.

I'd recommend something like

(defn assoc-user-id 
  [m user-id other-factors]
  (assoc m (key-for other-factors) user-id))

Where you implement key-for so that it selects the key based on other-factors.

Upvotes: 0

Related Questions