Welcome! Please see the About page for a little more info on how this works.

+3 votes
in Collections by

Hello! We recently found that when applying update-vals to a sorted map, it's converted back into an unsorted array/hash map due to the collection not being transientable. Example given below. As a note, this differs from medley/map-vals which preserves sorting and which we were using for this functionality previously to 1.11.

Is this intended behavior? If so, I'd be curious about the intent behind it :) Thanks!

user> (def m (sorted-map :a 0 :b 0))
;; => #'user/m
user> (type m)
;; => clojure.lang.PersistentTreeMap
user> (type (update-vals m inc))
;; => clojure.lang.PersistentArrayMap
user> (type (medley.core/map-vals inc m))
;; => clojure.lang.PersistentTreeMap

2 Answers

+3 votes
by
selected by
 
Best answer

Yes, this is intended behavior. There are tradeoffs with respect to genericness of map types and performance. Both options were considered and the end decision was that performance of the far more common types (hash and array map) was the higher concern here.

As the docstring says, this function returns "a new map" and map type is not preserved.

by
Just got bitten by this nonsense. Preserving the type is a logical expectation, given that many other functions do so, and there's not a word in the docstring to the contrary. The fact that docstring says that the function returns "a new map" in no way indicates that the type changes. We're dealing with immutable data structures, of course the function returns a new map, it could hardly do otherwise.
0 votes
by

the design is intentional, it's for performance. sorted maps are mainly for debugging

if you want your sorted map to work, use the code at the bottom of this answer

```clojure
(defn update-vals
  "m f => {k (f v) ...}

  Given a map m and a function f of 1-argument, returns a new map where the keys of m
  are mapped to result of applying f to the corresponding values of m."
  {:added "1.11"}
  [m f]
  (with-meta
    (persistent!
      (reduce-kv (fn [acc k v] (assoc! acc k (f v)))
        (if (instance? clojure.lang.IEditableCollection m)
          (transient m)
          (transient (empty m)))
        m))
    (meta m)))

(update-vals
  (sorted-map
    :d :e
    :b :c
    :h :a
    :a :g
    )
  str)
```

error

```clojure
1. Unhandled java.lang.ClassCastException
   class clojure.lang.PersistentTreeMap cannot be cast to class clojure.lang.IEditableCollection
   (clojure.lang.PersistentTreeMap and clojure.lang.IEditableCollection are in unnamed module of loader 'app')
```

code that works with sorted maps, but is slower

```clojure
(defn update-vals
  "m f => {k (f v) ...}

  Given a map m and a function f of 1-argument, returns a new map where the keys of m
  are mapped to result of applying f to the corresponding values of m."
  {:added "1.11"}
  [m f]
  (with-meta
    (reduce-kv (fn [acc k v] (assoc acc k (f v)))
      (empty m)
      m)
    (meta m)))
```
by
Thanks, but I see no reason `update-vals` couldn't have used sort-preserving logic on sorted maps while also maintaining performance for unsorted maps, so I'm wondering whether there was intent behind this function not supporting sorted maps as it's not documented as such AFAIK :)
by
the code i posted was edited to work on a sorted map, that's what `(transient (empty m))` does. the code throws an error. assoc! doesn't work on sorted maps i guess.

`(assoc! (sorted-map :a :b) :c :d)` throws and error
by
by
i was using transients in the first example. though in the second i messed up.
by
=>(assoc! (transient (sorted-map :a :b)) :c :d)
   No protocol method IEditableCollection.-as-transient defined for type object: {:a :b}
by
I don't understand this point:
> sorted maps are mainly for debugging
Sorted maps seem like a proper data structure to me. Not sure where debugging fits in here.
...