Boss: "Ship it!" You: "Let me compile it with :advanced optimizations..." Boss: "Sounds good!" ...one coffee later You: "Oops! It just broke! And I don't know why." Boss: "Don't tell me that a random person on the Internet was wrong again." You: (sad face) "Yep, they provided slightly outdated externs!"
Add these new power-macros to your tool belt:
ogetis a flexible, safe and guilt-free replacement for
ocallis a replacement for
(.call ...)built on top of
oapplyis a replacement for
(.apply ...)built on top of
Let's see some code examples first and then discuss the concepts:
Integrate with your project
Add oops dependency into your Leiningen's
project.clj or boot file.
Require macros in your namespaces via
oops.core ClojureScript namespace:
(ns your.project.namespace (:require [oops.core :refer [oget oset! ocall oapply ocall! oapply! oget+ oset!+ ocall+ oapply+ ocall!+ oapply!+]])) (oset! (js-obj) :mood "a happy camper")
Please note that we are not using
:refer-macros here. We rely on automatic macro refer inference in latest ClojureScript.
Also please be aware that oops uses clojure.spec which is available since Clojure 1.9. If you cannot upgrade to Clojure 1.9, you may stick with Clojure 1.8 and add this backported version of clojure.spec.
Otherwise pretty standard stuff. If in doubts, look at the sample project.
-- Darwin (with sunglasses on)
For example, the ClojureScript form
(.-nativeProp obj) will compile to
It works pretty well during development but there is a catch! When you naively write code like that, it might not survive advanced optimizations. Closure Compiler needs some information about which property names are safe to rename and which cannot be renamed because they might be referenced externally or dynamically via strings.
Someone at Google had a quick and bad idea. We could provide a separate file which would describe this information. Let's call it an "externs file"!
Externs from hell
I'm pretty opinionated about using externs. I hate it with passion. Here is the list of my reasons:
Say, authors of a useful (native) library don't provide externs file (usually simply because they don't use Closure Compiler). So there must come someone else who is willing to maintain an externs file for their library by following changes in the library. You want to use the library so now you made yourself dependent on two sources of truth and they don't usually move in a lock-step. Also that someone will probably sooner or later lose interest in maintaining the externs file and you have no way of telling if it is outdated/incomplete without doing a full code-review. And the worst part is that "someone" is very often you.
Incomplete (or outdated) externs files provide no feedback. Except that you suddenly discover that a new build is broken again and you are back to "pseudo-names fishing".
Externs have to be configured. Paths must be specified. Externs are not co-located with the code they are describing. It is not always clear where individual externs are coming from. Some "default" externs for standard browser/DOM APIs are baked-in Closure Compiler by default which might give you false sense of security or confuse assumptions about how this whole thing works.
Side-stepping the whole externs mess
What if I told you to ditch your externs because there is a simpler way?
(.-nativeProp obj) write
(aget obj "nativeProp") which compiles to
obj["nativeProp"]. String names are not
subject of renaming in advanced mode. And practically the same code runs in development and advanced mode.
I hear you. This looks dirty. We are abusing
aget which was explicitly documented to be for native array only.
Alternatively we could use
goog.object/get or the multi-arity
goog.object/getValueByKeys which looks a bit better,
but kinda verbose.
Instead of investing your energy into maintaining externs you could as well incrementally write a lightweight Clojure-style wrapper functions to access native APIs by string names directly. For example:
(defn get-element-by-id [id] (.call (aget js/document "getElementById") js/document id))
It is much more flexible than externs. You have full control and power of ClojureScript code here. And who knows, maybe later you will extract the code and publish it as a nice ClojureScript wrapper library.
Sounds good? With oops library the situation can be even better.
What if we had something like
aget but safer and more flexible?
I'm pleased to introduce you to
Be more expressive with selectors
The signature for
(oget obj & selector).
Selector is a data structure describing exact path for traversing into a native object
Selectors can be plain strings, keywords or for convenience arbitrarily nested collections of those.
Selectors are pretty flexible. The following selectors describe the same path:
(oget o "k3.?k31.k311") (oget o "k3" "?k31" :k311) (oget o ["k3" "?k31" "k311"]) (oget o [["k3"] "?k31"] "k311")
Please note the ".?" is a modifier for "soft" access (inspired by CoffeeScript's existential operator).
We expect that the key 'k31' might not be present and want
oget to stop and silently return nil in that case.
In case of
oset! you can use so-called "punching" for creation of missing keys on path. For example:
(oset! (js-obj) "!k1.!k2.!k3" "val")
That will create
k2 on the path to setting final
k3 key to
val. If you didn't specify the exclamation modifiers
oset! would complain about missing keys. This makes sense because if you know the path exists for sure
you don't want to use punching and that will ultimately lead to simpler code generated in :advanced mode (without any checks for missing keys).
Static vs. dynamic selectors
Dynamic selector is a selector which is not fully known at compile-time. For example result of a function call is a dynamic selector:
(oget o (identity "key"))
At runtime the form result is the same but generated code is less effective. Dynamic selectors should be very rare.
By default, oops assumes that you want to prefer static selectors and dynamic selectors are unintentional.
Compiler will issue a compile-time warning about "Unexpected dynamic selector usage".
To silence this warnings use "plus" version of
oget like this:
(oget+ o (identity "key"))
This way you express explicit consent with dynamic selector code-path.
Play it safe during development
By default, oops generates diagnostics code and does pretty intensive safe-checking in non-advanced builds. As you can see on the screenshots above you might get compile-time or run-time warnings and errors when unexpected things happen, like accessing missing keys or traversing non-objects.
Produce efficient barebone code in :advanced builds
By default, all diagnostics code is elided in :advanced builds and oops produces code similar to hand-written
(without any safety-checks).
You can inspect our test compilation transcripts to see what code is generated in different compiler modes.
Tailor oops behaviour
I believe oops has sensible defaults and there should be no need to tweak it under normal circumstances. Anyways, look at possible configuration options in defaults.clj.
As you can see, you can provide your own config overrides in the ClojureScript compiler options map
:external-config > :oops/config.
See example in cljs-oops-sample project.
Isn't accessing properties by string names slower?
Should I use cljs-oops with Closure Library (e.g. goog.something namespace)?
No! Use oops only for interop with external code which is not part of your :advanced build. That means for all code where you would normally need to write externs.
Closure Library is compatible with advanced compilation and identifiers get properly minified during compilation. You don't have to write any externs for it, so you don't have to use oops with it.
Second area where you want to use string names is when you work with JSON data objects (e.g. data received over a network). String names explicitly prevent minification of key names which must stay intact.
For better understanding please read this detailed article by Luke VanderHart.
How this approach compares to ClojureScript externs inference?
Externs inference is very recent feature and looks promising. It was introduced after I put all the effort into developing this library so my opinion is biased :-). I personally still prefer investing time into building light-weight ClojureScript wrapper libraries using string-names than dealing with externs (even if they are auto-inferred).