In this precedent post we have presented the skeleton of the javascript transpiled code of defrecord + deftype when they work together.


In this post we will review it by exploring the entire javascript transpiled code of defprotocol in clojurescript. We will reveal the secret of protocols in clojurescript.


Secret

Protocols

As defined in the documentation,

A protocol is a named set of named methods and their signatures, defined using defprotocol.

(defprotocol IFoo
  (fooA [this])
  (fooB [this]))

After creating your protocol, you can use deftype, defrecord or reify for defining its methods implementation. In our case, we will use deftype because the major difference between deftype and defrecord is that deftype provides just the functionalities implemented by the user, contrary to defrecord that implements a lot of things that will not help us to understand the generated javascript code. You can read more about defrecord here.

Let’s see the classical object oriented example of the animals’ cry:

Now, step by step, we will try to understand how the clojurescript compiler generates the protocol part in javascript. Let’s go with our basic protocol. Use KLIPSE to see the generated javascript code:

YES! this code seems very hard and incomprehensible! But don’t worry, it is simpler than it looks!

What do we got here? Two properties are added to the current user namespace:

  1. One named Animal, return an empty function! We will explain in another post its utility.
  2. A second property, named cry, return a function named like namespace$user$cry that receives an object. This part is the core of your protocol definition.

We will first focus on the first part of the transpiled code:

cljs.user.cry = (function cljs$user$cry(this$) {
    if ((!((this$ == null))) 
        && (!((this$.cljs$user$Animal$cry$arity$1 == null)))) {
        return this$.cljs$user$Animal$cry$arity$1(this$);
    } else {
        ...
    }
});

The cljs$user$cry function checks that the received object (this$ will have to be a deftype or a defrecord) isn’t null and if it implements the cljs$user$Animal$cry$arity$1 function. If these conditions are respected, launch the cljs$user$Animal$cry$arity$1 function.

Whence comes this cljs$user$Animal$cry$arity$1 function?

Let’s compile a deftype form to answer:

In the Bird prototype, the cljs$user$Animal$cry$arity$1 match the implementation of the cry function from the Animal protocol. The arity$1 part puts forward the number of arguments received by the cry function. Like this, your program knows which cry function to call. This example speaks for itself.

Now let’s focus on the second part:

...
var x__25043__auto__ = (((this$ == null)) ? null : this$);
var m__25044__auto__ = (cljs.user.cry[goog.typeOf(x__25043__auto__)]);
if (!((m__25044__auto__ == null))) {
    return m__25044__auto__.call(null, this$);
} else {
    var m__25044__auto____$1 = (cljs.user.cry["_"]);
    if (!((m__25044__auto____$1 == null))) {
        return m__25044__auto____$1.call(null, this$);
    } else {
        throw cljs.core.missing_protocol.call(null, "Animal.cry", this$);
    }
}
...

In this step there is 2 possible cases, the first when this$ is null and the second when you didn’t implement the cry function when you defined your deftype.

first case:

What is going on here? x__25043__auto__ is null, so m__25044__auto__ is null, the code is looking for cljs.user.cry["_"] which is, of course, null so the browser throw an exception!

But in which case is possible that cljs.user.cry["_"] won’t be null? tom@tomjack.co gave me the answer on the famous slack clojurians!

“… it is for (extend-type default …” @tomjack

You will find the answer using the extend-type macro on type default! Let’s see this macro in action:

In the current namespace, extend all clojure data type for it implements a default cry method of the Animal protocol. Let’s see the transpiled code of the extend-type macro:

WOW! It seems that we found our cljs.user.cry["_"]!! Let’s ask the question again:

What is going on here? x__25043__auto__ is null, so m__25044__auto__ is null, the code is looking for cljs.user.cry["_"] that means the code wants to know if, in the current namespace, a default implementation for cry function is available, if there is no, throws an error, else call this default cry function.

second case:

What is going on here? x__25043__auto__ is an object, so m__25044__auto__ is null and it is the same logic as the first case.

what do we have left?

We didn’t talk about this part:

...
var x__25043__auto__ = (((this$ == null)) ? null : this$);
var m__25044__auto__ = (cljs.user.cry[goog.typeOf(x__25043__auto__)]);
if (!((m__25044__auto__ == null))) {
    return m__25044__auto__.call(null, this$);
} else {
...

In which case m__25044__auto__ is not null? The response is very simple. According to the code, m__25044__auto__ is not null if, in the cry object, we can find a property which its name is the type of this$. Let’s present an example to meet this case:

Because you use extend-type not with default but with string type (or any another type), the compiler will add, to the protocol Animal, a property named string equals true. Additionally, the compiler will also add, to the cry object, a property named ‘string’ contains the implementation of the cry function from the Animal protocol for string objects as you can see in the transpiled javascript code:

...
(cljs.user.Animal["string"] = true);

(cljs.user.cry["string"] = (function (this$){
    return "A string can't be an animal!!!";
}));
cljs.user.cry.call(null,"hello");

So you obtain that goog.typeOf(x__25043__auto__) returns "string" which is a property of the cry object.

Great! Now, You belong to those who know the secret! ;)