Fork me on GitHub
#specter
<
2017-09-02
>
hmaurer11:09:49

Quick question: is there a set of “laws” that navigators are expected to follow? For example, I assume I should expect the following:

(= (select path structure) (transform path (fn [x] x) structure))
or
(= 42 (select path (transform path (fn [_] 42) structure)))
etc

nathanmarz11:09:45

@hmaurer well, those are both not true

nathanmarz11:09:19

:a for first one, ALL for second one shows falsehood

hmaurer11:09:23

@nathanmarz ha! how so? transforming a path with the identity shouldn’t leave the values under that path unchanged?

hmaurer11:09:55

oh sorry, my first example had a typo

hmaurer11:09:08

(= (select path structure) (select path (transform path (fn [x] x) structure)))

nathanmarz11:09:30

(transform (view inc) identity 1)

nathanmarz11:09:27

there's not any mathematical relations I can think of between select and transform

nathanmarz11:09:45

only important property is that select and transform traverse same values in same order

nathanmarz11:09:58

that's necessary for subselect

hmaurer11:09:11

@nathanmarz in the case of (view inc), the invariant

(= (select path structure) (select path (transform path (fn [x] x) structure)))
holds as far as I can see

hmaurer11:09:43

nevermind, it does not

hmaurer11:09:50

ok, thank you!

hmaurer11:09:42

I need to wrap my head around paths that apply functions while navigating 😄

nathanmarz11:09:15

yea, those ones are different

nathanmarz11:09:20

just view and transformed

hmaurer11:09:57

@nathanmarz what is your mental model for navigation if the model “navigating to a nested structure” is too limiting/unsuitable?

hmaurer11:09:02

if that question makes sense

nathanmarz11:09:58

well most of the time that is my mental model

nathanmarz11:09:06

including for substructure navigators like srange

nathanmarz11:09:31

I literally visualize moving from one portion of a data structure to another

nathanmarz11:09:37

it gets easier with practice

nathanmarz11:09:00

eventually it's like anything else in programming and its just instinctive

hmaurer14:09:23

@nathanmarz I am having a little bit of difficulty picturing the view example you mentioned about. When selected (reading) values, this makes sense. We are “viewing” the result of applying a function to every navigated value

hmaurer14:09:01

However the transform case seems a bit odd to me. I would expect transform to apply the inverse function of the function passed to view when re-constructing the data

hmaurer14:09:11

(aka flowing the data back “up the pipe”, if that makes sense)

hmaurer14:09:27

Am I fundamentally misunderstanding somethng here?

hmaurer14:09:51

(regardless of the fact that, afaik, there is no way to compute the inverse of an arbitrary function in clojure)

nathanmarz15:09:19

think of view as like a transform function that then continues navigating into the result of the transform

hmaurer15:09:17

@nathanmarz that clarifies things. So if my mental model looks like this:

[ 1 ] -- inc --> [ 2 ]
and I want to apply a transform to the value in the second box, which I then expect to be “flowed back” to the first box by inverting the computation, then parser is the way to go

nathanmarz15:09:37

that's right

hmaurer15:09:03

and view would stay navigated in the first box?

nathanmarz15:09:21

the only use case we could think of for the function+inverse was parsing, but it could be used for other things as well

nathanmarz15:09:32

view would stay at the second box

nathanmarz15:09:54

(transform [(view inc) STOP] dec 1) ;; => 2

hmaurer15:09:04

Wait, I would be navigated to the second box with view? How so? If I am at the first box and navigate to the second box by applying some computation (here view), and I then use setval to set the value on that path (the second box), shouldn’t I expect the computation to be inverted to determine the value of the first box?

hmaurer15:09:27

If view navigates to the second box, I don’t get why/how its behaviour differs from parser in the transform case

nathanmarz15:09:24

well it stays there

nathanmarz15:09:31

maybe "boxes" is not the right analogy

nathanmarz15:09:50

you can think of view not navigating at all, just changing the box it's at

nathanmarz15:09:01

and then the rest of the path continues with the new value

nathanmarz15:09:33

in that example I just showed you can see the dec function is never reached

nathanmarz15:09:39

but the view persists in the result

nathanmarz15:09:12

it's equivalent to (transform STOP dec (transform STAY inc 1))

hmaurer15:09:59

I see; thank you!

hmaurer15:09:52

ah, last but not least, do you have an example of a scenario where view is useful but parser would be unsuitable?

nathanmarz15:09:21

@hmaurer mostly I use it in selects, where its more concise than parser

nathanmarz15:09:38

plus there's no unparse-fn for those cases

nathanmarz15:09:09

one spot where I use it in a transform is needing to manipulate a string as a vector of characters

nathanmarz15:09:14

so I do a (view vec)

hmaurer15:09:29

In that case though parser‘s behaviour would make sense, since when manipulating a string as a list of characters you are essentially navigating into the substructure of that string, right?

hmaurer15:09:36

which you could then re-construct

nathanmarz15:09:39

it's actually for converting a vector of strings to a matrix of characters with some default rendering info attached to each character

nathanmarz15:09:20

yea you could have an unparse-fn for that kind of navigation, but in this case the goal is to have vector of characters in the output

nathanmarz15:09:37

(def card-back
[
".------."
"|//////|"
"|//////|"
"|//////|"
"|//////|"
"`------'"
])

nathanmarz15:09:12

basically that's how I specify "images" and the subsequent transform converts it to something that can be rendered

nathanmarz15:09:05

(def cards
  (into {}
    (for [v [2 3 4 5 6 7 8 9 :T :J :Q :K :A]]
      [v
       (into {}
         (for [[s template] {:clubs clubs-template :diamonds diamonds-template
                             :spades spades-template :hearts hearts-template}]
           [s (transform ALL #(apply str %)
                (setval [ALL (view vec) ALL (pred= \?)] (last (str v)) template))]
           ))])))

nathanmarz15:09:49

oh actually in this case it turns templates into vector of strings for each card value

nathanmarz15:09:44

forgot how this code works :/

nathanmarz15:09:03

in this case parser would work

hmaurer15:09:25

Mmh. Out of curiosity, is view the only (shipped with Specter) infraction to the invariant

(= (select path structure) (select path (transform path identity structure)))
?

hmaurer15:09:05

When thinking of navigation as “diving into structure” it seems that this invariant should hold; I am a bit bothered that view does not maintain it with transform

hmaurer15:09:15

but I am probably just being picky 😄

nathanmarz15:09:41

also transformed, nil->val

nathanmarz15:09:59

here's a better example of view transform use case: (transform [(view vec) ALL] (ichar-with-attrs prompt-attrs) prompt)

nathanmarz15:09:39

a "prompt" string is converted to a vector of characters with each character wrapped in a map also containing rendering attributes like color, font, etc.

nathanmarz15:09:47

could also do (transform ALL (ichar-with-attrs prompt-attrs) (vec prompt)), but the style with view would be useful if needed to do that transformation if had something like a map of prompts

nathanmarz15:09:23

invariants like that are important for monads where it really does affect the utility of the monad

nathanmarz15:09:45

other than the traversal order thing I mentioned, invariants like that aren't needed for navigation

hmaurer15:09:43

I was hoping to use such invariants to better understand what navigators can and can’t do

hmaurer15:09:56

But this discussion was very helpful 🙂

nathanmarz15:09:17

you could try reading the source code of navigators in specter

nathanmarz15:09:24

view is one of the simplest

nathanmarz16:09:07

navigators are just functions, so they have all the power a function has

hmaurer16:09:29

@nathanmarz of course, but I assume that if I want other people to be able to use the navigators I write they should follow some expected behaviours

hmaurer16:09:48

so as not to force them to completely swap their mental model around on every new navigator they encounter

hmaurer16:09:03

I might be overthinking the issue though; I’ll dig into the source-code!

nathanmarz16:09:37

the only types of navigators I've encountered are "subvalue" (e.g. keypath, nthpath, META, ALL), "substructure" (e.g. srange, submap, subset), "filter" (e.g. pred, selected?), and "view" (e.g. view, nil->val, transformed)

nathanmarz16:09:05

index-nav is an interesting example of a "subvalue" navigator because it navigates you to an implicit value

hmaurer16:09:14

@nathanmarz oh that’s interesting indeed. The doc on index-nav is missing from the wiki by the way

hmaurer16:09:20

Quick question: what is a “rich nav”?

nathanmarz16:09:00

@hmaurer yea the wiki is not up to date

nathanmarz16:09:12

the API docs are the most up to date, but the wiki has more examples

nathanmarz16:09:32

richnav exposes the full navigator interface when defining navigators

nathanmarz16:09:11

many navigators use defrichnav purely as an optimization

nathanmarz16:09:51

defnav wraps next-fn and puts vals in closure to pass along when it's called

nathanmarz16:09:10

with defrichnav you can avoid all that allocation/indirection by passing the vals yourself

nathanmarz16:09:36

unfortunately clojure doesn't optimize this case with inlining and neither does the JVM

nathanmarz16:09:50

although in theory a compiler could do it

nathanmarz16:09:42

so I do it manually for some cases where that optimization makes a difference – basically I try to do it wherever the navigation itself is very cheap so extra overhead will have noticeable performance impact

nathanmarz16:09:31

btw vals in that interface refers to "collected vals" collected with VAL, collect, and collect-one