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

+6 votes
in Collections by
retagged by

The & args in update, swap! and etc are very ergonomic and compose really well, so I was surprised to learn that update-keys and update-vals don't have that, they're only [m f].

What is the reason for this?

I think adding & args to these functions would be more consistent with the existing core functions and help avoid anon functions where not needed.

1 Answer

+1 vote

There are a number of functions similar to update-keys and update-vals in existing utility libraries (usually called map-keys / map-vals). afaik, those functions have not had the update extra arg semantics and that's not been something that people have asked for.

One difference is that update etc invoke the f on a single value, whereas update-keys and update-vals invoke f on many values. I'm not sure how often you would want to pass the same static trailing args to the function being applied to every key or value. I don't have any examples of that (but maybe that's just because the existing functions don't do this right now).

A workaround is of course to push that into the f via partial or anonymous function, so maybe that's something that could be searched for.

Do you have a specific use case where you needed this?

You can find some examples with:

* https://grep.app/search?q=map-keys%20%23%28&filter[lang][0]=Clojure
* https://grep.app/search?q=map-vals%20%23%28&filter[lang][0]=Clojure

Not all of these fit the pattern that would be needed but certainly some do.
edited by
Since the util libraries mostly treat these as if they were sequence functions the arity doesn't work for adding the `& args`, which I think is one reason.

My specific use-case was when cleaning up some records from a nested structure in an atom when I was done with the records, but wanted to keep the rest of the data around. Something like:

(def *state (atom {"table" {:schema (,,,) :records [,,,]}}))

;; I didn't expect this to blow up.
(swap! *state update-vals assoc :records [])

;; This works, but is arguably less elegant.
(swap! *state update-vals #(assoc % :records []))

I found these examples in the wild where this could have been useful:

* https://github.com/penpot/penpot/blob/develop/frontend/src/app/worker/import.cljs#L351
* https://github.com/penpot/penpot/blob/develop/backend/src/app/rpc/queries/files.clj#L273
* https://github.com/clojure/tools.analyzer/blob/master/src/main/clojure/clojure/tools/analyzer/passes/uniquify.clj#L31
* https://github.com/exoscale/deps-modules/blob/master/src/exoscale/deps_modules.clj#L154
* https://github.com/exoscale/deps-modules/blob/master/src/exoscale/deps_modules.clj#L156

It's probably more useful in practice for `update-vals` than `update-keys`, but for a contrived example you may want to postfix something to all the keys in a map.

(update-keys {"table" {}} str "__v1") => {"table_v1" {}}
edited by
[Edit: How do you properly format code on this site? I tried and couldn't figure it out, so I just left it as markdown.]

Would you still consider adding the variadic cases of `update-keys` and `update-vals`? I found this thread after being surprised that my use case wasn't supported and I think I have a pretty compelling example showing that this is desirable.

I have a data structure like this (and a predicate to be used later):
(def data {:foo {0 {:bar [10 42 11 12]}
                 1 {:bar [20 21 42 22]}
                 ,,, }})

(def my-special-pred (complement #{42}))

(Note that there can be arbitrarily many items in the `(data :foo)` map; the three commas are supposed to look like an ellipsis.)

Suppose I want to update the vector at `:foo 0 :bar`. This could be done with:
(update data :foo update 0 update :bar (partial filter my-special-pred))

In that case you could also just use `update-in` (and you probably should). But what if you want to do this for _all_ of the values in the :foo map, not just the one at 0? You _should_ be able to just use:
(update data :foo update-vals update :bar (partial filter my-special-pred))

But you can't, because `update-vals` only takes 2 arguments. Instead, you need to do something like:
(update data :foo update-vals #(update % :bar (partial filter my-special-pred)))

This is more inconvenient than it first looks; since anonymous function literals are not allowed to be nested, you can't use another one (e.g.) for the predicate. With a variadic `update-vals` though, you could do this:
(update data :foo update-vals update :bar (partial filter #(not= 0 (mod % 42))))

For the same reason, you can't easily use multiple "levels" of `update-vals`, like this:
(def data2 {0 {:top 200
               :bottom 201
               ,,, }
            1 {:left 300
               :right 301
               ,,, }})
(update-vals data2 update-vals inc)

There are various ways you can work around this, but by far the cleanest and best seems to be to just write your own `update-vals` function that wraps the existing one:
(defn update-vals [m f & args]
  (clojure.core/update-vals m #(apply f % args)))

With that defined, *all* of the examples in this comment work as expected. And since this is an "accreting change" (it's a strict superset of existing functionality), it's fully backwards compatible, and could (and should) be included in the core library.

(Also, for the record, the `data2` example is contrived, but the other one is structurally identical to actual code that I just wrote for my project, and I will be using the variadic `update-vals` wrapper.)
Alex Robbins: re formatting -- you can format questions and you can format answers, but comments are just text.
I was gushing about the wonderful multi-arity signature of the update functions in Clojure, today, and was surprised when this one didn't support it. Here's the case in question:

(defn scale [geometry factor]
  (update-vals geometry * factor))