specter

Latest version: 1.1.3
mbjarland 2017-12-19T16:08:54.000151Z

@nathanmarz apropos our conversation about warnings under clojure 1.9.0, a new version of midje has been released with an updated (direct) dep on specter:

[midje "1.9.1" :scope "test"]
   [com.rpl/specter "1.0.4" :scope "test" :exclusions [[org.clojure/clojure] [org.clojure/clojurescript]]]
     [riddley "0.1.12" :scope "test"]
the warnings are gone and the related issue on midje (https://github.com/marick/Midje/issues/427) closed

nathanmarz 2017-12-19T17:18:50.000191Z

@mbjarland great

rafael 2017-12-19T17:30:28.000268Z

Hi. Don't know if this is the right forum, I have a newbie specter question.

rafael 2017-12-19T17:31:15.000751Z

I wanted to filter out elements not matching the path for a transform call.

rafael 2017-12-19T17:31:38.000463Z

It's easier to explain in code. The following (specter/transform [specter/ALL (specter/must :a) (specter/must :b)] str [{:a {:b 10}} {:a {:z 42}}]) returns [{:a {:b "10"}} {:a {:z 42}}]

rafael 2017-12-19T17:32:00.000292Z

It called the transformation function for an element matching the path and returned the rest unchanged.

rafael 2017-12-19T17:32:27.000491Z

I'm wondering if is there anything I can do to make a similar call return [{:a {:b "10"}}] instead?

rafael 2017-12-19T17:32:53.000519Z

(filtering out the non-matching {:a {:b 10}} element)

tanzoniteblack 2017-12-19T17:41:14.000495Z

@rafael if you want to do a select and a transform in the same operation, you probably want to do select to get only the value you want returned and then use the view selector to make your transformation (https://github.com/nathanmarz/specter/wiki/List-of-Navigators#view)

tanzoniteblack 2017-12-19T17:42:45.000536Z

i.e. how I think of it: transform is for modifying in place a piece of a structure; select is for pulling out specific information I want from a structure; and select with the view selector is for doing both selecting only a piece and applying a transformation

rafael 2017-12-19T17:43:20.000248Z

Thanks @tanzoniteblack. I'm trying it out now

rafael 2017-12-19T17:45:23.000505Z

Naively applying view to the example above gets me:

(specter/select [specter/ALL (specter/must :a) (specter/must :b) (specter/view str)] [{:a {:b 10}} {:a {:z 42}}])
=> ["10"]

rafael 2017-12-19T17:45:51.000146Z

I wonder if is there a way to get [{:a {:b "10"}}] in the result.

tanzoniteblack 2017-12-19T17:46:40.000381Z

https://github.com/nathanmarz/specter/wiki/List-of-Navigators#transformed might do that?

👀 1
rafael 2017-12-19T17:46:41.000139Z

It would perfectly fine to chain a call to select and a another to transform, or whatever, but even then I'm having a hard time coming up with the right incantation 🙂

tanzoniteblack 2017-12-19T17:46:58.000048Z

not actually sure off the top of my head, sorry

rafael 2017-12-19T17:47:42.000471Z

No worries, thanks for the help. I'm checking out transformed now

tanzoniteblack 2017-12-19T17:54:20.000459Z

(specter/select [(specter/filterer (specter/must :a) (specter/must :b)) 
                                            specter/ALL
                                            (specter/transformed [:a :b] str)]
                                           [{:a {:b 10}} {:a {:z 42}}])
@rafael that appears to do what I think you want?

tanzoniteblack 2017-12-19T17:55:05.000091Z

filterer removes all items that don't have :b key inside the value at the :a key, and then for each item in that list, we're going to reach into the path :a then :b and run the function str

tanzoniteblack 2017-12-19T17:55:18.000732Z

probably not more efficient then the manual way of doing it? But no idea

rafael 2017-12-19T17:57:28.000728Z

The output looks right. I'm trying to digest it before applying to my real use case (which deals with a structure a bit more complicated than the example). Efficiency is not super important for my use case, I'm reaching for specter for the terseness in dealing with a deeply nested structure, so it should be alright

👍 1
nathanmarz 2017-12-19T18:06:14.000104Z

@tanzoniteblack you're looking for not-selected?

nathanmarz 2017-12-19T18:06:22.000126Z

(setval [ALL (not-selected? (must :a :b))] NONE data)

rafael 2017-12-19T18:09:21.000167Z

With not-selected and setval the complete example would then be:

(->> data
       (S/setval [S/ALL (S/not-selected? (S/must :a :b))] S/NONE)
       (S/transform [S/ALL (S/must :a :b)] str))
?

nathanmarz 2017-12-19T18:11:30.000132Z

yea, that would work

nathanmarz 2017-12-19T18:11:53.000013Z

for second part you don't need must anymore since they're guaranteed to be there by first part

rafael 2017-12-19T18:12:47.000652Z

Cool, now off to apply it to the real use case 🙂

rafael 2017-12-19T19:39:26.000153Z

Hi again. When applying to my actual use case things turned out to be more complicated. There are multiple S/ALL where we want to remove non-matching elements. Again, a code example is probably easier to explain:

(def data [{:a [{:b 10}]} {:a [{:b 20} {:z 42}]}])

  (->> data
       (S/setval [S/ALL (S/not-selected? (S/must :a) S/ALL (S/must :b))] S/NONE)
       (S/transform [S/ALL :a S/ALL :b] str))
results in
=> [{:a [{:b "10"}]} {:a [{:b "20"} {:z 42, :b ""}]}]
but I wanted [{:a [{:b "10"}]} {:a [{:b "20"}]}]

rafael 2017-12-19T19:39:52.000627Z

(it's a similar example to above, but with one extra nested sequence)

rafael 2017-12-19T19:40:16.000219Z

I could get the output I wanted with an extra setval:

(->> data
       (S/setval [S/ALL (S/not-selected? (S/must :a) S/ALL (S/must :b))] S/NONE)
       (S/setval [S/ALL (S/must :a) S/ALL (S/not-selected? (S/must :b))] S/NONE)
       (S/transform [S/ALL :a S/ALL :b] str))

rafael 2017-12-19T19:40:28.000345Z

This works, but I was wondering if there is any way of reducing the duplication

nathanmarz 2017-12-19T19:59:47.000342Z

@rafael (setval [ALL :a ALL (transformed (must :b) str) (not-selected? (must :b))] NONE data)

nathanmarz 2017-12-19T20:01:14.000262Z

do you also want to remove top level maps that don't have any inner maps under :a with :b?

rafael 2017-12-19T20:04:00.000218Z

Ideally yeah, If data is (def data [{:a [{:b 10}]} {:a [{:b 20} {:z 42}]} {:a [{:z 42}]}]) then I also need to remove the last {:a [{:z 42}]}

rafael 2017-12-19T20:06:13.000300Z

(trying to understand your suggestion above... thank's for taking the time to help)

nathanmarz 2017-12-19T20:13:33.000484Z

@rafael this does the trick:

(defdynamicnav with-matching [path]
  (if-path path
    path
    (terminal-val NONE)
    ))

(transform
  [ALL
   (with-matching (must :a))
   ALL
   (with-matching (must :b))
   ]
  str
  data)

rafael 2017-12-19T20:13:58.000533Z

Wow

nathanmarz 2017-12-19T20:19:11.000287Z

even better:

nathanmarz 2017-12-19T20:19:15.000251Z

(defdynamicnav with-matching [path]
  (if-path path
    (multi-path path (if-path path STOP (terminal-val NONE)))
    (terminal-val NONE)
    ))

(defdynamicnav with-non-empty [path]
  (multi-path path (if-path empty? (terminal-val NONE)))
  )

(transform
  [ALL
   (with-matching (must :a))
   (with-non-empty ALL)
   (with-matching (must :b))
   ]
  str
  data)

nathanmarz 2017-12-19T20:21:07.000110Z

with [{:a [{:b 10}]} {:a [{:q 20} {:z 42}]}] that removes the second map completely

nathanmarz 2017-12-19T20:21:53.000426Z

you can read about dynamic navs here: https://github.com/nathanmarz/specter/wiki/Specter's-inline-caching-implementation

nathanmarz 2017-12-19T20:21:59.000056Z

they're similar to macros

rafael 2017-12-19T20:26:46.000599Z

Looks like I have a lot of reading to do. Thanks

nathanmarz 2017-12-19T20:27:18.000291Z

actually this is a more flexible implementation I think:

(defdynamicnav with-matching [path]
  (if-path path
    (multi-path path (if-path path STOP (terminal-val NONE)))
    (terminal-val NONE)
    ))

(defn ^:direct-nav ensure-pred [pred-fn]
  (path (stay-then-continue (if-path (pred pred-fn) STOP (terminal-val NONE)))))

(transform
  [ALL
   (with-matching (must :a))
   (ensure-pred #(-> % empty? not))
   ALL
   (with-matching (must :b))
   ]
  str
  data)

nathanmarz 2017-12-19T20:27:41.000513Z

I may add some variant of these into specter

nathanmarz 2017-12-19T20:37:11.000355Z

I think this is the simplest solution:

(defdynamicnav ensure-matching* [path]
  (multi-path path (if-path path STOP (terminal-val NONE))))

(def ensure-matching (eachnav ensure-matching*))
    
(transform
  [ALL
   (ensure-matching (must :a) ALL (must :b))
   ]
  str
  data)