Converting a list of lists into a map of lists of lists in Clojure
I have a list of lists and would like to get this into a map where the key is one of the common values in the lists (animal name in this example). I know how to use into {} and for to create a map from a list but this is not exactly what I want. I want the key (animal's name) in the map to refer to a list of lists of these values.
I've created a list of lists
(def animals (list '("tiger" "fur" "yellow & black stripes") '("tiger" "weight" "150") '("tiger" "home" "India") '("elephant" "skin" "gray") '("elephant" "weight" "1500") '("elephant" "home" "Africa") '("frog" "skin" "green") '("frog" "diet" "insects")))
animals
(("tiger" "fur" "yellow & black stripes") ("tiger" "weight" "150") ("tiger" "home" "India") ("elephant" "skin" "gray") ("eleph开发者_JAVA百科ant" "weight" "1500") ("elephant" "home" "Africa") ("frog" "skin" "green") ("frog" "diet" "insects"))
This doesn't do exactly what I want (but almost)
(def animal-map (into {} (for [[name attribute value] animals] [name (list attribute value)])))
This results in only a single list for each key when I want multiple lists for each key
animal-map (map of lists)
{"tiger" ("home" "India"), "elephant" ("home" "Africa"), "frog" ("diet" "insects")}
This is want I want to end up with.
animal-map (map of list of lists)
{"tiger" ("fur" "yellow & black stripes") ("weight" "150") ("home" "India"),
"elephant" ("skin" "gray") ("weight" "1500") ("home" "Africa"),
"frog" ("skin" "green") ("diet" "insects")}
Regarding your desired outcome, the final map you posted is not doing what you think it's doing. In this map:
{"tiger" ("fur" "yellow & black stripes") ("weight" "150") ("home" "India")}
This is not a map with one key and three values, this is a map with two keys ("tiger"
and ("weight" "150")
) and two values.
The following doesn't answer your question directly, but a word on idiomatic Clojure. You will have an easier time in Clojure if you choose the proper data structures for the job at hand.
For your initial data, you should probably use vectors instead of lists unless you have a good reason to use lists. Vectors perform better for many operations (e.g. random access); you don't need to quote a vector like you do a list; and vectors signal that you're dealing with data rather than code.
Lists are appropriate if your data has to grow from the front rather than the back, or if you're generating source code as data (in macros for example).
[["tiger" "fur" "yellow & black stripes"]
["tiger" "weight" "150"]
["tiger" "home" "India"]
...]
For your final data, consider using a map of strings to maps instead of a map of strings to lists. It's easy to create maps-of-maps using assoc-in
because it creates intermediary maps for you. Then you could access the sub-keys of your data using get-in
.
To access the attributes of the animals in your map of strings to lists, you would have to do a linear search over the list of attributes. This will be awkward to code and perform poorly.
user> (def animal-map (reduce (fn [m [animal k v]]
(assoc-in m [animal k] v))
{} animals))
#'user/animal-map
user> animal-map
{"frog" {"diet" "insects", "skin" "green"},
"elephant" {"home" "Africa", "weight" "1500", "skin" "gray"},
"tiger" {"home" "India", "weight" "150", "fur" "yellow & black stripes"}}
user> (get-in animal-map ["frog" "diet"])
"insects"
Finally, I'm not sure where this data is coming from, but consider using keywords instead of strings for some of these keys. It may or may not be appropriate for the names of the animals, but it's almost certainly appropriate for the attributes of the animals (weight, home, diet).
If your keys are keywords, you don't even need get-in
, you can use the keywords as functions directly, or use ->
. You can't do this with strings.
user> (def animal-keyword-map (reduce (fn [m [animal k v]]
(assoc-in m [(keyword animal) (keyword k)] v))
{} animals))
#'user/animal-keyword-map
user> animal-keyword-map
{:frog {:diet "insects", :skin "green"},
:elephant {:home "Africa", :weight "1500", :skin "gray"},
:tiger {:home "India", :weight "150", :fur "yellow & black stripes"}}
user> (:weight (:tiger animal-keyword-map))
"150"
user> (-> animal-keyword-map :tiger :weight)
"150"
By conjoining your list into {} you a replacing duplicate keys.
(reduce (fn [m [k & vals]] (assoc m k (conj (m k) vals))) {} animals)
to avoid the later entries replacing the previous ones you need to create a bunch of maps and then merge them together in a controlled manner. hooray! we have merge-with
for just this job!
first we convert the list into a sequence of maps by using the first string in each list as the key and the rest of them as the value.
(map #(hash-map (first %) (rest %)) animals)
({"tiger" ("fur" "yellow & black stripes")}
{"tiger" ("weight" "150")}
{"tiger" ("home" "India")}
{"elephant" ("skin" "gray")}
{"elephant" ("weight" "1500")}
{"elephant" ("home" "Africa")}
{"frog" ("skin" "green")}
{"frog" ("diet" "insects")})
then lets try just merging them with list
because its the frist thing to come to mind.
(apply merge-with list (map #(hash-map (first %) (rest %)) animals))
{"frog" (("skin" "green") ("diet" "insects")), "elephant" ((("skin" "gray")
("weight" "1500")) ("home" "Africa")), "tiger" ((("fur" "yellow & black stripes")
("weight" "150")) ("home" "India"))}
This looks a lot more like it though nested too deeply. not to fret we can prevent the nesting by moving the list creation into the inner map:
(apply merge-with concat (map #(hash-map (first %) (list (rest %))) animals))
{"frog" (("skin" "green") ("diet" "insects")), "elephant" (("skin" "gray") ("weight"
"1500") ("home" "Africa")), "tiger" (("fur" "yellow & black stripes") ("weight" "150")
("home" "India"))}
I wont be able to match the desired output exactly because a map can only have one value for a key. to "tiger" needs to map to a list of pairs. instead of more than one pair.
精彩评论