Clojure All The Way - Deep Knockout Part 4 of mini-series
By the end of the previous post we’ve implemented
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
strings. In type-safe language like C# or Java it would be
clj->js function recursively converts our Clojure
Arrays. I.e. not only outer
vector is converted, but each element of it (inner
strings) is also converted to
Array. The recursion stops there because
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
Note: Code samples include only interesting parts, full source code is available below.
Shallow outer vector conversion
We want to stop converting inner
Arrays. I.e. we don’t need a deep recursive conversion of outer
clj->js provides. Instead we want to convert outer
Array and leave its elements intact. The result of this step should become
This is easy, we can use
into-array function in place of
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"></td> <td data-bind="text: $data"></td>
$data access syntax does not work for Clojure
If we examine
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)
but it’s not very user-friendly to say the least.
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-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
<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
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">
Array. Period. No kidding.
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
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
,  ... [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
1, etc. It’s no better than
WARNING: Danger Zone
This is why the rest of this post is not practical [for now] but hopefully still entertaining.
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 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
length field and
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)))))
get function (lines 7-11) checks if accessed property name is
length and returns
v if it is (line 10). Otherwise (line 11) it simply delegates to
get function to fetch individual element.
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
The Finish Line
The last change is to modify
observable-ref once again, this time replacing
into-array calls with our newly-created
(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!
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
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 :-).
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