Clojure 1.2 introduces two very remarkable features – Protocols and Datatypes. Clojure is defined in terms of abstractions and various implementations of those abstractions. For example, vectors, maps, lists, sets in Clojure implement the sequence abstraction which lets us treat any of those data structures as sequences.
Until recently it was not possible feasible to define and implement such core abstractions in Clojure itself; one had to drop down to Java (or C#) for those tasks, but not anymore
Clojure 1.2 now has excellent facilities for defining and implementing similar abstractions in a highly dynamic manner while maintaining fantastic performance characteristics.
In this post, I will give you a brief overview of these new features and will show you how they are useful.
Protocols
Protocols in Clojure are similar to Java Interfaces, though not quite. Basically a protocol is a contract, a set of functionalities without any implementation. Let’s consider a simple protocol –
(defprotocol Fly "A simple protocol for flying" (fly [this] "Method to fly"))
So here we have a trivial protocol Fly which declares a method fly which takes one argument ‘this’ (which is actually the type implementing the protocol itself). In case of all methods defined via Protocols, the first argument is always the implementing type itself. The name ‘this’ is just a convention; it could be ‘self’, etc. or anything.
When we declared the Fly protocol, two new vars were created. One is ‘Fly’, the protocol itself, and the other is ‘fly’ which is a polymorphic function that will get called when we execute it on an implementation of Fly.
Right now, if you try to execute the method ‘fly’ on any object, you will get an exception because no types are implementing that protocol yet, which brings us to the next topic, DataTypes.
DataTypes
Traditionally in Clojure whenever we wanted to have some kind of record or a property-only Class, we used maps or struct-maps. Those serve the purpose perfectly well in most cases but the problem was that those maps didn’t have any type information attached to them. As a result, we had to put some extra keys in maps to help us determine the type of a record before we could dispatch methods. There were some obvious performance limitations too; being vanilla maps, they were never as fast as Plain Old Java Objects (POJOs). Enter deftype and its cousin defrecord.
In Clojure 1.2 we can define our own types using defrecord like this -
(defrecord Bird [nom species])
Boom! We have a custom type, Bird with two fields, name and species. We can now instantiate a Bird like this -
(def crow (Bird. "Crow" "Corvus corax"))
We can access the fields of the Bird instance by treating it like
a normal map -
user› (:nom crow) "Crow"
user› (:species crow) "Corvus corax"
We can also add/remove/modify keys in a record like we would do with a
normal map.
(def sparrow (assoc crow :nom "Sparrow" :species "Passer domesticus"))
This will create a new immutable instance of Bird with different data. Note that since Clojure records are persistent and immutable, the original crow instance is not affected.
Now to make the Bird fly. We already have a protocol called Fly. We need to implement the protocol so that our birds can actually fly. One way to do that is to put the protocol implementation inline with the record definition itself -
(defrecord Bird [nom species] Fly (fly [this] (str (:nom this) " flies..."))
So easy, right? If we now create another instance of Bird, it will actually be able to fly -
user› (def kiwi (Bird. "Kiwi" "Apteryx australis")) #'user/kiwi user› (fly kiwi) "Kiwi flies..."
Great! But what happens to the Crow, and Sparrow? We created those instances when the Bird record didn’t have any implementation of the Fly protocol. You might face similar issues when you don’t have control over the code which defines the record/class. You will need to extend those types dynamically with implementation of a protocol. Enter extend-type. extend-type (and its cousin extend-protocol) allows us to implement protocols on pre-existing types. Consider the following example -
(defprotocol Walk "A simple protocol to make birds walk" (walk [this] "Birds want to walk too!")) (extend-type Bird Walk (walk [this] (str (:nom this) " walks too..."))
We just added an implementation of the Walk protocol to the existing type Bird. All new Bird instances created from now on will be able to Walk and Fly.
user› (def hummingbird (Bird. "Hummingbird" "Selasphorus rufus")) user› (fly hummingbird) "Hummingbird flies..." user› (walk hummingbird) "Hummingbird walks too..."
Cool, right? At times you might require a anonymous object which implements some protocol or interface. You could utilise those objects in cases where you just need an object which implements a given protocol but you don’t care about its type. Clojure 1.2 gives you reify. reify allows us to create one-off anonymous objects which implement one or more protocols.
user› (fly (reify Fly (fly [_] "Swine flu..."))) "Swine flu..."
Woah! Clojure can make Pigs fly
Jokes apart, what we just did was very interesting. We just created an anonymous type which implements the Fly protocol and called the fly method on it; and it flu[sic]
We could implement multiple protocols in the same reify statement too,
like this -
(def pig (reify Fly (fly [_] "Swine flu...") Walk (walk [_] "Pig-man walking..."))) user› (fly pig) "Swine flu..." user› (walk pig) "Pig-man walking..."
Beautiful. reify is quite similar to proxy and it is now recommended to use reify instead of proxy wherever possible because reify is much faster than proxy.
Before I finish off, let me explain the differences between defrecord and deftype. defrecord creates a new type and implements a few core Clojure interfaces like that of the persistent map, hashcode, keyword accessors, etc. If you are using deftype, Clojure will not implicitly implement any interface not provided by the user. In short, if you are using deftype, you will have to implement your own accessors, hashcode, etc. In most cases defrecord should suffice, but in other cases like when you need mutable fields, use deftype.
There is some in-depth explanation of Protocols and Datatypes on the Clojure website which you should consult if you need more information.
Bonus Material
Making Java Strings fly and walk
user› (extend-type java.lang.String Fly (fly [this] "See me fly?") Walk (walk [this] "Yes, that's me walking!")) nil user› (walk "foo") "Yes, that's me walking!" user› (fly "bar") "See me fly?"
PS – I wrote this today because I was sitting at home, sick. There are possibly some mistakes in this post; in which case, please let me know.
Tags: clojure, Free Software, functional programming, Programming
Sorry you are sick, but this is well written! Easily understandable.
Thanks, Keith
Thanks very much for this, it really helped to clarify these new concepts in my head. You write in a very clear and lucid manner.
Also, I do hope that you get well soon. Being sick is never any fun…
Thanks for your kind words, Sam. This will encourage me to write more
Hi Baishampayan
Very well written and articulated … Would suggest for you to perhaps write a book on clojure
Really decent post… I love it. Keep ‘em coming…
Nice writeup.
Questions:
* with 1.2, is clojure becoming OOish ? or is it just clojure with support for defining and implementing abstractions ?
* With extend-type, does clojure promote ruby like monkey patching and open classes ?
* Can you give some real life examples to better understand the difference between regular interfaces and clojure protocols ?
Thanks for the gentle introduction. Protocols, datatypes including reify, deftype and defprotocol are starting to make sense to me now.
I hope you feel better soon.
Chetan: Haha, writing a book is tough, let’s see
Manoj: Clojure will never become OOish is the traditional sense, nevertheless sometimes facilities to create abstractions and their implementations in Clojure itself becomes necessary. Protocols and Datatypes were created to pave the way for “Clojure in Clojure”; which is Clojure written in Clojure. It wasn’t possible before, but now it is. In the near future, Clojure’s core abstractions will be rewritten in Clojure itself. Not all OO mechanisms are bad, more so when using them is not compulsory. And no, Protocols don’t allow monkey-patching existing objects. Watch this nice video by Stuart Halloway which explains the motivation behind Clojure protocols and its key differences between interfaces, etc. – http://vimeo.com/11236603
Raju: Thanks
Outstanding blog post. Best job yet, at least for me, of explaining this important set of new features of the Clojure language.
Bring on Clojure 1.2!
..and here’s hoping that you get well soon.
Publicfarley: Thanks
Thanks Jim
Great post, giving nice examples about protocols and types
Do we still require the ‘dot’ notation when instantiating a defrecord type?
Also in the example that you have given about adding/removing/modifying keys what benefits are there by using ‘assoc crow’ instead of simple Bird
Hey Adityo!
Thanks! The dot-notation is required to keep the syntax identical to that of creating instances of Java classes. records are after all plain Java classes with a constructor, some methods (implementations of protocols) and ‘final’ instance properties.
You create a new instance when you need one. Sometimes you want to “modify” an existing instance to get another instance. It all depends on your use-case. Consider this -
(def data {:name “BiG” :email “b.ghose@argonaut.com”})
(def fixed-data (assoc data :name “BG”))
vs.
(def fixed-data {:name “BG” :email (:email data)})
Which one would you choose? Depends on how you like it, of course
Hope this helps.
“It wasn’t possible before, but now it is.”
Just a nit, but it was always technically possible using proxy, gen-interface, and gen-class, but not practically so.
:f
You are right Fogus. I should have used the word “feasible” instead of “possible”. Thanks
Its been said before but again, best post about protocols. One comment, in the protocol implementations specified in the defrecord, instead of using (:nom this) to access the nom parameter, you may want to mention that you can just write nom.
This:
(defrecord Bird [nom species]
Fly
(fly [this] (str (:nom this) ” flies…”))
Turns to this:
(defrecord Bird [nom species]
Fly
(fly [_] (str nom ” flies…”))
Thanks, Brent
You are right, I should have shown the other way too. It was on my agenda, but I somehow forgot about it. Thanks for pointing it out.
Hi, when is Clojure 1.2 due for release? And what other new features will it have?
…treating defrecord instances like a normal map? Be careful and try (crow :nom) instead of (:nom crow) in Clojure 1.2.1-RC1.
thanks for this post! helped a lot
Excellent. Gentle Introduction.