In this post I discuss the new feature in Clojure that just made its way in the recently released 1.2. I am not going into what Protocols are - there are quite a few nice articles that introduce Clojure Protocols and the associated
deftypeforms. This post will be some random rants about how protocols encourage non intrusive extension of abstractions without muddling inheritance into polymorphism. I also discuss some of my realizations about what protocols aren't, which I felt was equally important along with understanding what they are.
Let's start with the familiar
Showtype class of Haskell ..
> :t show
show :: (Show a) => a -> String
Takes a type and renders a string for it. You get
showfor your class if you have implemented it as an instance of the
Showtype class. The
Showtype class extends your abstraction transparently through an additional behavior set. We can do the same thing using protocols in Clojure ..
The protocol definition just declares the contract without any concrete implementation in it. Under the covers it generates a Java interface which you can use in your Java code as well. But a protocol is not an interface.
Adding behaviors non-invasively ..
I can extend an existing type with the behaviors of this protocol. And for this I need not have the source code for the type. This is one of the benefits that ad hoc polymorphism of type classes offers - type classes (and Clojure protocols) are open. Note how this is in contrast to the compile time coupling of Java interface and inheritance.
(show [i] (.toString i)))
We can extend an interface also. And get access to the added behavior from *any* of its implementations .. Here's extending
(show [v] (.toString v)))
(show [12 1 4 15 2 4 67])
> "[12 1 4 15 2 4 67]"
And of course I can extend my own abstractions with the new behavior ..
(defrecord Name [last first])
(defn name-desc [name]
(str (:last name) " " (:first name)))
(name-desc (Name. "ghosh" "debasish")) ;; "ghosh debasish"
(show (Name. "ghosh" "debasish")) ;; "ghosh debasish"
Protocols help you wire abstractions that are in no way related to each other. And it does this non-invasively. An object conforms to a protocol only if it implements the contract. As I mentioned before, there's no notion of hierarchy or inheritance related to this form of polymorphism.
No object bloat, no monkey patching
And there's no object bloat going on here. You can invoke
showon any abstraction for which you implement the protocol, but
showis never added as a method on that object. As an example try the following after implementing
(filter #(= "show" (.getName %)) (.getMethods Integer))
will return an empty list. Hence there is no scope of *accidentally* overriding some one else's monkey patch on some shared class.
Not really a type class
Clojure protocols dispatch on the first argument of the methods. This limits its ability from getting the full power that Haskell / Scala type classes offer. Consider the counterpart of
Showin Haskell, which is the Read type class ..
> :t read
read :: (Read a) => String -> a
If your abstraction implements
Read, then the exact instance of the method invoked will depend on the return type. e.g.
> [1,2,3] ++ read "[4,5,6]"
The specific instance of
readthat returns a list of integers is automatically invoked here. Haskell maintains the dispatch match as part of its global dictionary.
We cannot do this in Clojure protocols, since it's unable to dispatch based on the return type. Protocols dispatch only on the first argument of the function.