Welcome! Please see the About page for a little more info on how this works.

+8 votes
in Protocols by
retagged by

The clojure.protocols/nav function has signature:

[coll k v]

I don't understand what coll, k and v would be. Can someone explain? Maybe give an example of calling nav with realistic arguments?

For instance, what is coll. Is it supposed to be the result of a prior call to datafy? Thus is coll what we will navigate into, or is that v?

I assume k is the required information to drill down.

Now there is v, so again, I'm not sure if this is meant to be the value we are navigating into? But if so, what is coll ? If v isn't that, than I don't understand what it should be. Isn't the value what we want nav to return, why do we provide it as well?

Thank You

2 Answers

+6 votes

Let's start by assuming that we have some object of type Thing and it has an implementation of datafy that produces a Clojure hash map that describes the thing.

myThing -> datafy -> description-hash-map

Now we can navigate that hash map using get: (get description-hash-map :foo) and it will produce some value that we'll call foo-val.

Having gone from the Thing world to the Clojure data world via datafy, we can use all the regular Clojure functions to navigate around in the data world but we usually need a way to navigate back into the Thing world at points that are represented by elements in the data world.

That's where nav comes in. For any (get description-hash-map :foo) that produces some foo-val value, we can navigate back to the equivalent point in the Thing world by calling (nav description-hash-map :foo foo-val) and that will produce some new object in the Thing world.

We can then get back to the Clojure data world by calling datafy on that new object, using get to navigate around in the data world, and nav to navigate back to equivalent points in the Thing world.

myThing -> datafy -> description-hash-map

description-hash-map -> get -> more-data

description-hash-map -> nav (with more-data) -> someNewThing

If datafy produced a vector, you call nav with the vector, the index (vectors are associative on their indices), and the element at that index.

In all cases, nav may or may not care about the value argument (and, indeed, it may or may not care about the key argument either... but that's less likely). Note that when datafy produces a data representation of some object, it adds the original object as metadata on the data representation -- and that metadata is available to nav via the coll argument so that it can get at the original object if it needs it.

If you want to look at a concrete example in the real world, see next.jdbc's datafy, nav, and :schema machinery.

I did look at next.jdbc's implementation, and `coll` is never used in the implementation of `nav`: https://github.com/seancorfield/next-jdbc/blob/1b93d3a04ba033bb627e493df28f32330dfeb2e5/src/next/jdbc/result_set.clj#L643-L664

Which confuses me even more :(
True. In that case, the "new Thing" can be found using only the key and the value -- because the foreign key relationship is based on the key and the value (and possibly the schema option), not the row of data that that it was found in.

So nav may or may not care about the value, the key, or even the collection :)
coll is, in particular, often a handy place to store meta that will be needed during nav.
Alright, so I gather then that:

- `v` is what we are navigating into.
- That `coll` is supposed to be result of a call to `datafy`
- That `v` is supposed to be contained inside `coll`, retrievable by `k`

Thus a usage scenario would be:

(let [coll (datafy some-obj)
      k :next-thing
      v (get coll k)]
  (nav coll k v))

This would return another thing (aka, arbitrary datafiable Object), which is obtained from navigating into `v`.

In that sense, `v` is supposed to be a hyperlink, and it is not really a collection, which is why we navigate to what the hyperlink points too, and that returns another thing which we can later datafy as well if we want too, and possibly navigate further into.

So a full example could be:

(let [url (as-url "http://clojure.org")
      clojure-page-as-coll (datafy url) ...

At this point, depending on the datafy implementation for URL, we could assume we got back the following:

 :links {:get-started (as-url "https://clojure.org/guides/getting_started")
         :overview (as-url "https://clojure.org/about/rationale")

And so say we wanted to navigate into [:links :overview] we would do:

(let [url (as-url "http://clojure.org")
      clojure-page-as-coll (datafy url)
      overview-url (get-in coll [:links :overview])
      overview-page (nav clojure-page-as-coll [:links :overview] overview-url) ...

So at this point, we have datafied a URL as a Clojure coll. And we've drilled-down into it using normal Clojure collection functions. And when we reached another Navigable value, we called nav on it to fetch the next page.

The only thing I'm a bit confused still. Is if you look at my example, I assume `nav` returns a datafied result. But Sean mentioned it shouldn't. But that doesn't even make sense in my example. I start with a URL. I can nav into that URL to get the page back, but there are no `coll` context yet. And I could have nav return something that is not already data, but I don't have such a thing, and making one up just cause seems weird.

So I'm assuming here that `nav` can return data or datafiable things, it doesn't really matter. And it is assumed datafy will be called on it no matter what, but if it is already data, datafy will just return the data, since that's the default impl for Map.

So you'd continue as such:

(let [url (as-url "http://clojure.org")
      clojure-page-as-coll (datafy url)
      overview-url (get-in coll [:links :overview])
      overview-page (nav clojure-page-as-coll [:links :overview] overview-url)
      overview-page-as-coll (datafy overview-page)]

Is this correct? Or should you actually start with `nav` ? Or maybe my example is a bad one, since it seems `datafy` is redundant in my case, as someone could have just done:

(let [clojure-page (nav nil nil (as-url "http://clojure.org"))
      overview-url (get-in clojure-page [:links :overview])
      overview-page (nav clojure-page [:links :overview] overview-url)]

I would expect nav to match get, not get-in, and I'd expect it to work off data types (since it is protocol-based).

(comments are very poor for sharing code so I'm not going to try, but I'll build an example based on your URI navigation and come back with that later)
"I assume `nav` returns a datafied result."

No, should return an object.

"I can nav into that URL to get the page back"

No, you should first datafy the URL to get data, then nav into that data (there's your coll).

"And I could have nav return something that is not already data, but I don't have such a thing, and making one up just cause seems weird."

If you have data already, it's perfectly fine to return it from nav. But here I think you'd want to return an "as-url" object, whatever form that takes.
> No, you should first datafy the URL to get data, then nav into that data (there's your coll).

When you say nav into the the data is where I get confused, because the collection can be navigated into, such as what get, get-in, first, nth, etc. can do. In order to navigate to the elements of the data or the nested collections within it. But also, the elements can be navigated into if they are a form of hyperlink in that the rest of the content needs to be fetched remotely and/or rendered.

At first I assumed nav did both, and even datafied the result. But now it seems nav should only do the latter?

And if nav only does the latter, there are a lot of use cases for which it is going to be a noop correct? Like an identity function. Like in the case where nothing needs to be fetched or rendered, but only converted to data as well. Like if you have Objects nested inside Objects?

For that, I assume datafy isn't supposed to be deep. But is that true? Should datafy deeply and recursively datafy the whole Object and all contained objects except for cases where more data must be fetched remotely or rendered?
Thing world -> datafy -> Clojure data world

Clojure data world -> get, etc -> Clojure data world

Clojure data world -> nav -> Thing world

It's changing of "worlds" that is important here.

Either (or both) of datafy / nav can perform some complex transformation to shift worlds.

For example, (datafy (java.net.URL. "...")) could "transform" the Java object into the content at that URL, rendered as data.

Thing world -> datafy -> Clojure data world.

Clojure navigation within that data could let you get to each of the links in that page (as a Clojure data structure representing it, such as {:link "..."}).

Clojure data world -> get, etc -> Clojure data world

nav could then be applied on those same various paths to turn {:link "..."} into a java.net.URL. object.

Clojure data world -> nav -> Thing world

Now we're back in Thing world, with a java.net.URL. object... and we can datafy this to get the referenced content as a data structure and get back into Clojure data world.

If you haven't tried REBL, I strongly recommend that you do so -- it really will make this much clearer (or, at least, I _hope_ it will).
+1 vote

nav is a generic framework for navigation. Given a "container" (coll) and some selection in that container, "navigate" to that selection. For containers, k makes sense (like maps), for some it's unnecessary and the selection value is sufficient. As a generic framework, both are provided as you might need both to do selection.

As far as what exactly the inputs and the output are, this is a totally contextual answer. nav is a tool that is fit to the purpose at hand so without knowing a particular goal, it's impossible to be specific. You are typically both the caller and implementer (via instance metadata) of nav and the important thing is that they match.

In REBL, nav is used in service of the browse action where you have something selected in the left pane and you "browse" into it. Other uses may do different things (serializing an object graph, whatever).

I was under the impression that adding support for datafy/nav to something I own would add support for it inside REBL. Is that not the case? Because for that, I was assuming there should be a convention for "drilling into", such that say, when you eval some expr, datafy is called on it and the result rendered in the content pane, and if it is a known type which can be selected into by REBL, such as a map or vector, that selecting an idx in coll and then nav forward would call nav on the datafied expr with `k` being idx and `v` being val, and in turn this result would become the new expr, and it would repeat.

I can see how datafy/nav can be much more generic, basically use it as you want within your own convention, but within the context of REBL, is there a convention to follow?

Thanks again!
edited by
I actually tried this out with REBL, and it looks like:

1. The content pane calls datafy on the selected expr
2. Nav forward calls nav where `coll` is the return of datafy from #1
3. And `k` is the `idx` selected from the content pane
4. And `v` is the `val` selected from the content pane

From that and all the answers here, I gather this gist https://gist.github.com/didibus/2a2a62d365f93d55db4fb27f46ecef89 seems like a reasonable use of datafy/nav?
nav is not needed in your example: https://github.com/seancorfield/datafy-nav-example/blob/master/src/datafy_test/alt.clj

That produces the exact same results without implementing nav (because nav's default behavior on a hash map is to just return the value v (which is (get coll k) because that's how you invoke nav in this context).