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.

Note: Code samples include only interesting parts, full source code is available below.

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:

1 (defn observable-ref [r]
2   (let [state (ko/observable (into-array @r))]
3     (add-watch r state (fn [obs _ _ new] (obs (into-array new))))
4     state))

The only changes to this function compared to the previous post are into-array calls in lines 2 and 3 where clj->js calls used to be.

Of cause if we try to render HTML now it won’t work because our KO bindings look like this:

<td data-bind="text: $data[0]"></td>
<td data-bind="text: $data[1]"></td>

and $data[0] access syntax does not work for Clojure vectors.

Accessing PersistentVector elements from JavaScript code

If we examine vector’s “class” PersistentVector from pure JavaScript perspective we’ll see the nth method that we want to call to get vector’s elements. But it looks like this:

cljs$core$IIndexed$_nth$arity$2: function (coll, n)

We certainly can call it from JavaScript, and it works:

$data.cljs$core$IIndexed$_nth$arity$2($data, 0)

but it’s not very user-friendly to say the least.

Fortunately JavaScript is a very flexible and dynamic language. We can easily extend PersistentVector with helper get method that we need:

1 (aset (aget [] "__proto__")
2       "get"
3       (fn [index]
4         (this-as this
5                  (nth this index))))

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:

<td data-bind="text: $data.get(0)"></td>
<td data-bind="text: $data.get(1)"></td>

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:

<tbody data-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.

ECMAScript 6 Proxy API

ECMAScript 6 draft specifies Direct Proxies - a mechanism to create a proxies that:

… enable ES programmers to represent virtualized objects (proxies). In particular, to enable writing generic abstractions that can intercept property access on ES objects.

Exactly what we need!

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:

 1 (defn vector-as-array
 2   "Creates JS Proxy around Clojure vector to make it look like JS array,
 3   without copying data"
 4   [v]
 5   (.create js/Proxy
 6            (js-obj
 7              "get" 
 8                (fn [_ prop]
 9                  (case prop
10                    "length" (count v)
11                    (get v prop)))
12              "getPropertyDescriptor"
13                (fn [obj prop]
14                  (.getOwnPropertyDescriptor js/Object js/Array prop)))))

The get function (lines 7-11) checks if accessed property name is length and returns count of vector v 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:

(defn observable-ref [r]
  (let [state (ko/observable (vector-as-array @r))]
    (add-watch r state (fn [obs _ _ new] (obs (vector-as-array new))))
    state))

And it’s hard to believe, but IT WORKS!

Conclusion

Here is a quick summary of our accomplishments:

  • We’ve marshaled Clojure data structure created on the Server all the way to the Client using edn encoding which is a natural textual representation for Clojure data.
  • We’ve received this data on the client using core.async to make our code look sequential
  • Then we’ve edn-decoded response and called regular Clojure functions on received data to extract and sort the headers
  • We’ve animated our HTML page by manipulating only Clojure data structures
  • Finally we’ve created KO bindings directly to Clojure data without EVER converting it to JavaScript.

Granted, the final step required sacrifices (using experimental JavaScript features), and this is why I’ve mentioned above that it depends on reader’s point of view if final goal was achieved or not.

I’ve certainly achieved my goal of learning new interesting technologies and I had lots of fun along the way!

And if anyone ever reads this, I hope you did too :-).

Source code

Full source code can be found on GitHub.

If you want to build and run it locally, execute:

git clone https://github.com/Dimagog/dimagog.github.io.git -b ClojureAllTheWay4 --single-branch ClojureAllTheWay4
cd ClojureAllTheWay4
lein ring server


blog comments powered by Disqus

Published

31 July 2013
by Dmitry Kakurin

Tags