Open snoe opened 2 years ago
What about a more general solution:
(defn flip [f]
(fn [coll g & args]
(f #(apply g % args) coll)))
It might need a better name, but it changes a collection function like map
to work more like update
. So:
;; original
(map #(update % :price inc) items)
;; flipped
((flip mapv) items update :price inc)
This allows us to write:
(update m
:users
(flip mapv)
update :orders
(flip (comp set map))
update :items
(flip mapv)
update :price #(str "$" %))
And:
(update m
:users
(flip (comp #(filterv (comp seq :orders) %) mapv))
update :orders
(flip (comp set map))
update :items
(flip filterv)
(comp even? :price))
I like it, flip
also probably has more precedence ( https://lodash.com/docs/#flip ), although I like that your version moves coll position rather than reversing args.
I would recommend against the name flip
for that reason, but it's a very clever function that I would also use.
What about flop
? 😃
:shipit:
After having thought about it a while, my current inclination is to use tilt
:
(defn tilt
"Converts a collection function of 2 arguments, such as map or filter, into a
function with an argument signature that can be used with update or swap!.
For a collection function collf, the expression ((tilt collf) coll f & args)
is equivalent to: (collf #(apply f % args) coll)."
[collf]
(fn [coll f & args]
(collf #(apply f % args) coll)))
Just throwing in my 2c, I've been calling this fn to->
because it can be used to adapt thread-last-style fns (e.g. map
) to thread-first style:
(defn to->
"Adapt f to thread-first"
[x f & args]
(apply f (concat args (list x))))
It turns out that this fn is exactly equivalent to ->>
except that it's a fn rather than a macro so it can be passed to higher-order functions.
Accordingly, I also have a thread-last version:
(defn to->>
"Adapt f to thread-last"
[f & args]
(let [x (last args)]
(apply f x (butlast args))))
Unless I'm mistaken, @maxrothman, your to->
and to->>
functions are different in purpose from tilt
, and don't quite solve the same problem.
You're right, upon closer inspection, to->
only allows you to traverse one level of nesting. For example, (update {:a [1 2 3]} :a to-> map inc)
works fine, but (update {:a [{:x 1}]} :a to-> map update :x inc
does not.
I wonder if there's a way to have the same fn/macro work for both update
-like usecases and threading usecases. I suppose with tilt you could always do (-> [1 2 3] ((tilt map) inc))
, but that feels a little ugly. Maybe multiple arities? Maybe the problem is that map
treats additional args as additional collections, rather than as extra static args to the mapping fn like update does? I'll think on it more.
Mostly I wanted to point out that adapting fns to thread-first/last is an adjacent problem, and that there might be utility in having the opposite-threading version as well.
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
, andupdate-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 haveorders
, that haveitems
, that each have aprice
Suppose you want to change all the item prices to strings with a dollar sign pre-pended. You'd need to do something like:
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 toupdate
butcoll-k
points at a collection anditem-update-fn
is applied to each item in the collection.Using this, our highly nested, tedious, and error-prone processing can flatten out completely.
POC Implementation
This could be written without
update-items*
but being able to pass themapping-fn
can be useful: