Great languages are those that offer orthogonality in design. Stated
simply it means that the language core offers a minimal set of
non-overlapping ways to compose abstractions. In an earlier article A Case for Orthogonality in Design
I discussed some features from languages like Haskell, C++ and Scala
that help you compose higher order abstractions from smaller ones using
techniques offered by those languages.
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 defrecord and deftype forms. 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 Show type class of Haskell ..
> :t show
show :: (Show a) => a -> String
Takes a type and renders a string for it. You get show for your class if you have implemented it as an instance of the Show type class. The Show
type 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.
Extending java.lang.Integer with SHOW ..
(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 clojure.lang.IPersistentVector ..
(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 show on any abstraction for which you implement the protocol, but show is never added as a method on that object. As an example try the following after implementing SHOW for Integer ..
(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 Show in 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 read that 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.