contains?
will check (in the case of a map) for a key within a map. (contains? data v)
returns true
when v
is :a
or :f
. (contains? data :c)
will return false
as :c
is not a top level key in data
.
Problem
I want to ask "containment" questions about the keys at ALL levels within a map. I want to be able to determine not just that :a
and :f
are within the map but also that :b
, :c
, :d
, :e
, and :g
are "contained" as well. Conversely, I want to know that :z
is not a key within any map in data
.
Solution
(defn- add-children-metadata [m]
(->> m
vals
(map (comp :all-keys meta))
(apply clojure.set/union (-> m keys set))
(assoc (meta m) :all-keys)
(with-meta m)))
(defn map->containment-map
"Sets :all-keys metadata for every map in m; :all-keys holds every key in this map and for all submaps"
[m]
(-> (fn [acc k v]
(assoc acc k (if (map? v)
(-> v map->containment-map add-children-metadata)
v)))
(reduce-kv {} m)
add-children-metadata))
Usage
(def data {:a {:b nil
:c {:d nil
:e {}}}
:f {:g nil}})
(def contained-data (map->containment-map data))
Note that data
and contained-data
are equal.
(= data contained-data)
The difference is that contained-data
has the :all-keys
key within its metadata.
(-> contained-data meta :all-keys)
Examples
With this metadata, you can ask containment questions about keys at all levels. Each submap within contained-data
also contains :all-keys
in their metadata.
contained-data
contains the key :f
(-> contained-data meta :all-keys (contains? :f) (= true))
contained-data
does not contain the key :x
(-> contained-data meta :all-keys (contains? :x) (= false))
Key :a
under contained-data
has keys :b
:c
:d
and :e
(-> contained-data :a meta :all-keys (= #{:b :c :d :e}))
Conclusion
That's it. You can use the :all-keys
metadata to either:
- Do fast containment lookup of all keys within a map.
- Get a sequence of all (unique) keys within a map.