Search code examples
clojure

What's the best way to store an object and its features in clojure?


An object has at least its position and dimensions but potentially other fields, eg.

(def obj1 '(cup1 x 0 y 0 z 0 width 10 height 10))
(def obj2 '(cup2 x 0 y 0 z 0 width 10 height 10))
(def objs '(obj1 obj2))

What's the most efficient way to store these type of objects if I want to access and modify values, compare their names or dimensions and allow me to potentially add new fields in the future?

Is there something like an ordered dictionary like in python?


Solution

  • If all the fields are of equal importance, then a list of maps is great, but finding a single object will be slow since you'll have to walk through the whole list looking for it. If the "name" of each object is a unique/primary key then you could use a map of maps instead for very easy (and fast) lookup of individual objects by their names.

    (def objs
      {"cup1" {:x 0 :y 0 :z 0 :width 10 :height 10}
       "cup2" {:x 0 :y 0 :z 0 :width 10 :height 10}})
    
    ;; retrieve by name (will error if name is not found)
    (get objs "cup1")
    
    ;; add (will replace any existing obj with the same name)
    (assoc objs "cup3" {:x 0 :y 0 :z 0 :width 10 :height 10})
    
    ;; remove (will not error even if name is not found)
    (dissoc objs "cup3")
    
    ;; update by name (will error if name is not found)
    (assoc-in objs ["cup1" :x] 5)
    
    ;; get all objects without names (i.e. list of maps)
    (vals objs)
    
    ;; get all objects with names
    (map (fn [[n obj]]
           (assoc obj :name n))
         objs)
    

    Note that everything in Clojure is immutable unless otherwise specified, so all the above operations will result in a separate data structure, leaving the original untouched. Nothing is actually being "updated" or "overwritten" or "removed" - Clojure is simply returning you the view of what the data would look like if those things were done.

    The main Clojure website has a section dedicated to domain modelling using maps.

    To convert the data structure in your question into this map of maps, you could:

    ;; from question
    (def obj1 '(cup1 x 0 y 0 z 0 width 10 height 10))
    (def obj2 '(cup2 x 0 y 0 z 0 width 10 height 10))
    (def objs '(obj1 obj2))
    
    ;; since '(obj1 obj2) contains merely symbols, not references to obj1 and obj2,
    ;; it will be necessary to store it like this instead:
    (def objs [obj1 obj2])
    
    (defn map-keys
      "Applies function f to each key of map m.
       Ex: (map-keys inc {1 100 2 200}) -> {2 100 3 200}"
      [f m]
      (reduce-kv (fn f-of-k [acc k v]
                   (assoc acc (f k) v))
                 (empty m) m))
    
    (defn symlist->map
      "Converts the OP's symbol list into a keywordized map with a stringified name.
       Ex: (symlist->map '(a x 1)) -> {:name \"a\" :x 1}"
      [symbol-list]
      (update (map-keys keyword
                        (apply hash-map (conj symbol-list 'name)))
              :name str))
    
    (def obj-lookup
      (->> objs
           (map (comp (juxt :name identity) symlist->map))
           (into {})))
    
    ;; now you can
    (get obj-lookup "cup1")
    

    The above converts the name to a string, but you can leave names as symbols by removing the (update ... :name str) from symlist->map (and you can leave even the property names as symbols by removing the (map-keys keyword ...)). In fact nearly anything can be a key in a Clojure hash map (even entire other maps, although that wouldn't be a common use case) as long as the things correctly implement hashCode and equals.

    If for whatever reason you need to get back to the original structure, something like this will work:

    (map (fn [[obj-name obj-properties]]
           (seq (reduce (fn symbolify-entry [acc [k v]]
                          (conj acc (symbol k) v))
                        [(symbol obj-name)] obj-properties)))
         obj-lookup)
    

    And obviously it's even simpler if you had decided to leave all keys as symbols everywhere, because no conversion back to symbols is needed:

    (map (fn [[obj-name obj-properties]]
           (conj (flatten (seq obj-properties)) obj-name))
         obj-lookup)