Clojure All The Way - Deep Knockout Part 4 of mini-series

This is a fourth and final article in mini-series. The goal of mini-series is to create a Client-Server environment where Clojure data structures are pervasive and we don’t have to deal with JSON and JavaScript objects in Clojure/ClojureScript land (as much as possible).

Building up on the previous post we will achieve much deeper integration of Clojure data structures with Knockout.js. But it comes with a price …

Our Goal

By the end of the previous post we’ve implemented observable-ref that completely hides Clojure-to-JavaScript conversion call to clj->js function, but we know it’s still there.

Maybe it’s silly to worry about it, but I still do. So what if we set our mind to get rid of it completely, and have “turtles all the way down”? That’s my goal for this post and it’s up to the reader to decide if it is achieved or not :-).

Know the Enemy

Let’s have a detailed look at what our Clojure data structures are and what our “enemy” (for this post) clj->js does for us.

The data we are rendering (headers of HTTP request) is a Clojure vector of 2-element vectors of strings. In type-safe language like C# or Java it would be Vector<Vector<string>>.

The clj->js function recursively converts our Clojure vector of vectors into JavaScript Array of Arrays. I.e. not only outer vector is converted, but each element of it (inner vector of strings) is also converted to Array. The recursion stops there because string representation is the same across ClojureScript and JavaScript and no conversion is needed.

The Easy part - Inner vectors

Later in this post it will become clear why this is the easy part, but let’s start with inner vectors.

Shallow outer vector conversion

We want to stop converting inner vectors into Arrays. I.e. we don’t need a deep recursive conversion of outer vector that clj->js provides. Instead we want to convert outer vector to Array and leave its elements intact. The result of this step should become Array of vectors of strings.

This is easy, we can use into-array function in place of clj->js in observable-ref function:

We use empty vector [] in line 1 to get to PersistentVector’s prototype. And add a new method get that takes an index and returns element at that index for this. this-as in line 4 gives us access to this from ClojureScript.

Now we can change our KO bindings in HTML file to use our new get method:

Not too bad, and rather straight-forward. At this point our Client works again and we can see the table of HTTP headers rendered correctly.

The Hard part - Outer vector

The only piece of data conversion logic we have left is into-array calls that convert our outer vector into JavaScript Array. If we get rid of it - we’ve reached our goal.

Why it’s hard

But this is where it becomes tricky. The problem is that we use KO’s foreach binding to iterate over our collection of headers:

<tbodydata-bind="foreach: $root">

And foreach expects it to be JavaScript Array. Period. No kidding.

I’ve tried to teach KO to iterate over custom collection, and I’ve asked KO group. It’s not possible for now.

Should we give up?

At this point we still have achieved a tangible improvement: the elements of our collection are not converted anymore, so even when we create a copy of outer vector it is a shallow copy: only the “skeleton” is copied, but elements are reused. And they potentially have a bigger memory footprint.

However there is a saying:

If the mountain will not come to Muhammad, then Muhammad will go to the mountain.

In our case: if foreach would not take anything but Array then vector should become an Array. Or at least pretend to be one.

Now how do we pretend? The access pattern from KO is to read length property first and then read fields [0], [1] ... [length-1]. We can easily add all these fields to our vector but it defeats the purpose: we would copy all vector elements into fields 0, 1, etc. It’s no better than into-array call.

If only there was a way to intercept field access calls in JavaScript …

WARNING: Danger Zone

To the best of my knowledge there is no portable way to intercept field access in JavaScript. So we’ll have to use JavaScript “Experimental Features” that are already implemented by some modern browsers (Firefox and Chrome support what we need), but are not standard yet and may not even be enabled by default.

This is why the rest of this post is not practical [for now] but hopefully still entertaining.

If you are willing to proceed and use Chrome, you must navigate to chrome://flags URL (enter it manually in address bar, Chrome blocks navigation to special pages from random web sites :-), find “Enable Experimental JavaScript” feature and enable it.

Wrapping outer vector in a Proxy

A new helper function vector-as-array makes vector sort-of look like Array in a narrow sense required by KO. This is by no means a full proxy to emulate Arrays, just a bare minimum required for our purposes.

KO only needs 2 things from Array: length field and 0, 1, etc. fields. Well, we can give it what it wants:

The get function (lines 7-11) checks if accessed property name is length and returns count of vectorv if it is (line 10). Otherwise (line 11) it simply delegates to vector’s get function to fetch individual element.

The getPropertyDescriptor(lines 12-14) is required for when KO does “reflection” on our “array” (like checking if “length” property exists) and simply delegates calls to js/Array.

The Finish Line

The last change is to modify observable-ref once again, this time replacing into-array calls with our newly-created vector-as-array function: