A few weeks ago I shared my confusion about writing object-oriented ClojureScript and a little library called cljs-painkiller. Thanks to the awesome Clojure / ClojureScript community I soon learned much better ways to do it.
Painkiller Example
I complained that I had to write ClojureScript that looks like this:
(defn Bag [] (this-as this (set! (.-store this) (array)) this)) (set! (.. Bag -prototype -add) (fn [val] (this-as this (.push (.-store this) val)))) (set! (.. Bag -prototype -print) (fn [] (this-as this (.log js/console (.-store this))))) (def mybag (Bag.)) (.add mybag 5) (.add mybag 7) (.print mybag)
Wrong! Soon after that article appeared on DZone, David Nolen (@swannodette) showed me a few snippets in plain ClojureScript that do the same thing:
(deftype Bag [store] Object (add [_ x] (.push store x)) (print [_] (.log js/console store))) (defn bag [arr] (Bag. arr))
(defn bag [store] (reify Object (add [this x] (.push store x)) (print [this x] (.log js/console store))))
Much better, isn’t it? And it compiles to fairly idiomatic, interoperable JavaScript, not some higher-level magic.
I am in two minds about the need to expose store
like this. One one hand, it makes all the mutable things explicit. On the other, it means you’re exposing much “private” stuff to the consumer, even requiring it from him. To deal with that, you can hide array creation in constructor function:
(defn bag [] (Bag. (array)))
(defn bag [] (let [store (array)] (reify Object (add [this x] (.push store x)) (print [this x] (.log js/console store)))))
Backbone Example Revisited
When I was just starting with ClojureScript, I shared an example with Backbone integration. Then I complained it was downright unusable with any less trivial Backbone code.
Here’s what my sample looked like:
(def MyModel (.extend Backbone.Model (js-obj "promptColor" (fn [] (let [ css-color (js/prompt "Please enter a CSS color:")] (this-as this (.set this (js-obj "color" css-color)))))))) (def my-model (MyModel.))
It turns out it can be rewritten to:
(def MyModel (.extend Backbone.Model (reify Object (promptColor [this] (let [ css-color (js/prompt "Please enter a CSS color:")] (.set this (js-obj "color" css-color))))))) (def my-model (MyModel.))
Much noise gone. It seems that such reify
call is the way to go in this case.
Saved?
I love being proven wrong by the community, and clearly there are better ways to do it than I thought initially. Actually, when I started my adventure with ClojureScript I was quarreling with the compiler – now I finally am beginning to know what I’m doing.
ClojureScript requires some ceremony around object creation, separating behavior from state and its initialization. In some contexts it is too restrictive, in some it’s just fine.
My last example is the Knockout spike where I had JavaScript like this:
function AppViewModel() { this.firstName = ko.observable("Bert"); this.lastName = ko.observable("Bertington"); this.fullName = ko.computed(function() { return this.firstName() + " " + this.lastName(); }, this); this.capitalizeLastName = function() { var currentVal = this.lastName(); this.lastName(currentVal.toUpperCase()); }; } ko.applyBindings(new AppViewModel());
To those unfamiliar with Knockout, firstName
etc. are methods. Particularly interesting methods are fullName
and capitalizeLastName
. Here we have method created by call to ko.computed
, wrapping a function that references other methods of this
object. Not so bad in an OO language…
… but in ClojureScript apparently the best you can do is what I did back when I was getting started:
(def my-model (js-obj "firstName" (.observable js/ko "Bert") "lastName" (.observable js/ko "Bertington") "fullName" (this-as this (.computed js/ko (fn [] this (.firstName this)), this)))) (.applyBindings js/ko my-model)
I don’t like this at all. This is where I think macros are really necessary. Could be the painkiller, or some special macros just for Knockout integration.
ClojureScript does not always need painkiller, but as your code gets more interesting macroing your way out may be inevitable. I guess you don’t always need to write such OO code, but when you do – be ware.