First off, this might be out of scope for the project, but I do think it is a powerful addition and still fits under pure, and general purpose. Second, I'm not sure if my implementation is the most performant, nor if the name is the best.
Rationale
It is very common to find highly nested data-structures.
This is why get-in
, assoc-in
, and update-in
exist.
Unfortunately, these fall apart if you need to process items of a collection within the structure. Even worse is if you have a multiple collections to in the tree. I believe this difficulty was a major motivator for large DSLs like meander or spectre.
I've been using this function for a number of years and find it invaluable when I know the shape of a data-structure but need to traverse collections and update-in
is insufficient.
Comparison
Here's an example, if I have a map with users
, that have orders
, that have items
, that each have a price
{:users [{:id 1 :orders #{{:items [{:price 1} {:price 4} {:price 2}]}}}
{:id 2 :orders #{}}]}
Suppose you want to change all the item prices to strings with a dollar sign pre-pended. You'd need to do something like:
(update
{:users [{:id 1 :orders #{{:items [{:price 1} {:price 4} {:price 2}]}}}
{:id 2 :orders #{}}]}
:users
(fn [users]
(mapv (fn [user]
(update user :orders (fn [orders]
(set (map (fn [order]
(update order :items (fn [items]
(mapv (fn [item]
(update item :price #(str "$" %)))
items))))
orders)))))
users)))
;; =>
{:users [{:id 1, :orders #{{:items [{:price "$1"} {:price "$4"} {:price "$2"}]}}}
{:id 2, :orders #{}}]}
Writing this example out, I made a number of mistakes: I tried to nest function literals, I forgot to pass the coll to many of the map
calls, I forgot that :orders
is supposed to be a set.
Here's the signature for update-items
, similar to update
but coll-k
points at a collection and item-update-fn
is applied to each item in the collection.
(defn update-items [m coll-k item-update-fn & item-update-args])
Using this, our highly nested, tedious, and error-prone processing can flatten out completely.
(update-items
{:users [{:id 1 :orders #{{:items [{:price 1} {:price 4} {:price 2}]}}}
{:id 2 :orders #{}}]}
:users
update-items
:orders
update-items
:items
update
:price
#(str "$" %))
;; =>
{:users [{:id 1, :orders #{{:items [{:price "$1"} {:price "$4"} {:price "$2"}]}}}
{:id 2, :orders #{}}]}
POC Implementation
(defn update-items*
[m k mapping-fn item-update-fn & item-update-args]
(update m k
(fn [coll]
(mapping-fn (fn [item]
(apply item-update-fn item item-update-args))
coll))))
(defn update-items
[m k item-update-fn & item-update-args]
(apply update-items*
m
k
(fn [mapper coll]
(into (empty coll) (map mapper) coll))
item-update-fn
item-update-args))
This could be written without update-items*
but being able to pass the mapping-fn
can be useful:
(update-items*
{:users [{:id 1 :orders #{{:items [{:price 1} {:price 4} {:price 2}]}}}
{:id 2 :orders #{}}]}
:users
(comp #(filterv (comp seq :orders) %) map)
update-items
:orders
update-items*
:items
(comp vec #(sort-by :price %) filterv)
(comp even? :price))
;; =>
{:users [{:id 1, :orders #{{:items [{:price 2} {:price 4}]}}}]}