Fork me on GitHub
#specter
<
2016-06-08
>
quantisan13:06:36

I'm trying to replace my group-by/`merge-with` logic, any suggestion on how to transform:

{:1000 {:a {:sends 1}}
 :2000 {:a {:clicks 1 :opens 1 :sends 1}}
 :3000 {:b {:sends 1 :opens 1}}}
into
{:a {:sends 2 :opens 1 :clicks 1}
 :b {:sends 1 :opens 1}}

nathanmarz13:06:15

I don't see a way to express that in terms of navigation

quantisan13:06:13

here's my current ugly solution:

(->> {:1000 {:a {:sends 1}}
      :2000 {:a {:clicks 1 :opens 1 :sends 1}}
      :3000 {:b {:sends 1 :opens 1}}}
     (select [ALL LAST])
     (group-by (comp first keys))
     (transform [ALL LAST]
                #(select [ALL LAST LAST] %))
     (transform [ALL LAST]
                (partial apply merge-with +)))

nathanmarz13:06:52

(defn merge-with* [f maps]
  (apply merge-with f maps)
  )

(merge-with* (fn [& maps] (merge-with* + maps)) (vals data))

nathanmarz13:06:13

maybe you could define a merge-with navigator somehow

nathanmarz13:06:39

like from a sequence of maps you navigate to a sequence of values for any given key

nathanmarz13:06:07

so then you could do

(transform [MERGED-MAPS MERGED-MAPS] sum (vals data))

nathanmarz13:06:02

yea it works

nathanmarz13:06:04

(defnav MERGED-MAPS []
  (select* [this structure next-fn]
    ;;TODO: fill this in
    )
  (transform* [this structure next-fn]
    (apply merge-with (fn [& vals] (next-fn vals)) structure)
    ))


(defn sum [vals] (reduce + vals))



(def data
 {:1000 {:a {:sends 1}}
  :2000 {:a {:clicks 1 :opens 1 :sends 1}}
  :3000 {:b {:sends 1 :opens 1}}})

(transform [MERGED-MAPS MERGED-MAPS] sum (vals data))

nathanmarz13:06:37

I do what I can

nathanmarz13:06:28

oops, had sum written incorrectly there

nathanmarz13:06:35

now it's fixed

aengelberg19:06:00

@nathanmarz I agree with your point about OMIT. My main reasoning for proposing it was that it allows you to control which layer of the path you want to pass NONE to. For example:

(transform [ALL (if-path ... STAY OMIT) ALL (if-path ... STAY OMIT) :a (if-path ... STAY OMIT) ...] ...)
Is there a more idiomatic way to do this without coupling the navigation and the transformation as tightly?

nathanmarz20:06:48

well I think what you want is a single operation that can do multiple transforms

nathanmarz20:06:15

@aengelberg as it stands now transform does a single transform to all navigated elements

nathanmarz20:06:51

I've thought a bit about this, and basically you could imagine a different transform API that does not take in a transform-fn at the top-level

nathanmarz20:06:35

(multi-transform [ALL (if-path even? (setval-transform 2) (setval-transform NONE))] data)

aengelberg20:06:17

The question being whether it's worth an exception being made for the "specter abstracts the path from the transformation" idea

nathanmarz20:06:56

that's basically why I've held off on this

nathanmarz20:06:35

and also I've gotten by just chaining multiple transformations

nathanmarz20:06:48

(->> data (transform ...) (transform ...))

aengelberg20:06:49

But that's less efficient right?

nathanmarz20:06:00

yea, which is why I keep thinking about this 🙂

nathanmarz20:06:31

that said, I don't think places where I do that are bottlenecks for me

aengelberg20:06:41

Btw I've been thinking about the new select semantics, there may be an opportunity to introduce transducers

aengelberg20:06:59

Since the continuation is basically a "reducer"

nathanmarz20:06:59

got an example?

aengelberg20:06:37

(select [ALL] (map inc) [1 2 3])
=>
[2 3 4]

aengelberg20:06:18

(select-reduce conj [] [ALL] [1 2 3])

aengelberg20:06:26

just some ideas. the latter would be achieved by storing the reduced value in a cell and repeatedly updating it

nathanmarz20:06:42

that last one would output [1 2 3]?

nathanmarz20:06:02

I'm not following

aengelberg20:06:51

If I understand the new select semantics properly, the continuation (or "final next-fn") is currently "dump into a vector". I'm proposing letting the user specify an alternative for that continuation

nathanmarz20:06:12

well there are different continuations for the different selects

nathanmarz20:06:36

you basically want "traverse" exposed

aengelberg20:06:00

I just looked up traverse, didn't realize that was generalizing the continuation internally. Yeah, that's basically what I'm saying

nathanmarz20:06:49

I haven't completely internalized transducers yet

aengelberg20:06:09

transducer: reducer -> reducer

nathanmarz20:06:30

but basically it's like an aggregator that looks at its input one value at a time

nathanmarz20:06:47

and specter traverse looks at values one at a time

aengelberg20:06:36

exposing traverse isn't necessarily related at all to transducers

nathanmarz20:06:05

but you're saying you could just plug in a transducer for that continuation fn

aengelberg20:06:28

err, my posting two examples above probably made things confusing.

aengelberg20:06:59

My first example (providing (map inc)) was supplying a transducer to modify the hardcoded reducer

aengelberg20:06:05

The second example (providing conj) was replacing the reducer

aengelberg20:06:15

You could potentially combine both

aengelberg20:06:43

Except there isn't much advantage of providing both since the user can just call (my-transducer my-reducer) and get a reducer to pass in

nathanmarz20:06:08

what if specter could just integrate into the normal transducer api?

nathanmarz20:06:34

(eduction xf (traverse [ALL :a even?] data))

nathanmarz20:06:55

or even (eduction xf (traverse [ALL :a even?]) data)

nathanmarz20:06:18

actually the former probably makes more sense

aengelberg20:06:11

the latter I think would actually be (sequence (traverse [ALL :a even?]) data)

aengelberg20:06:42

or (sequence (comp xf (traverse [ALL :a even?])) data)

aengelberg20:06:01

Would traverse apply the transformation to each input form?

aengelberg20:06:20

err select, not transformation

nathanmarz20:06:22

here I'm thinking traverse would return an object that can be reduced

aengelberg20:06:35

that makes more sense

aengelberg20:06:43

i.e. implements IReduce

aengelberg20:06:08

and we wouldn't have to worry about clojure 1.6 compatibility I don't think

nathanmarz20:06:36

although I think I might drop that soon anyway

nathanmarz20:06:00

1.9 is coming out and I'm sick of cljx 😉

aengelberg20:06:52

I was working on a project last year but got stuck on leiningen issues. the idea was a plugin that would take cljc files and output clj / cljs similar to cljx

nathanmarz20:06:12

that would have helped a lot

aengelberg20:06:54

the main motivation actually was for instaparse; we don't really want to pull in instaparse-cljs until a non-deprecated cross-compile solution exists

aengelberg20:06:53

(that also works with <1.7)

aengelberg20:06:39

maybe i'll pick it back up soon, the main blocking issue was leiningen's built-in tools.reader was too old (no longer an issue probably)

nathanmarz20:06:02

any idea how many people are still on 1.6?

aengelberg20:06:55

my previous job was still on 1.6 well into the lifetime of 1.8

aengelberg20:06:40

just one data point, not sure how realistic it is to continue support for 1.6 especially with cider now on >1.7