8
votes

Having recently seen a presentation of Clojure Protocols, I was quite impressed by the clean way extensions to existing types can be done this way. However, I was pretty sure to have already seen a similar way of doing this in some other language and after some time I found out it was Groovy Categories.

Compare this:

 @Category(String) ​class StringCategory {
   String lower() {
     return this.toLowerCase()
   }
 }

 use (StringCategory) {
   println("TeSt".lower())
   assert "test" == "TeSt".lower()
 }

to the Clojure Protocol equivalent (taken from mikera's answer below and tested in ideone.com)

 (defprotocol Lowerable
   (lower [x]))

 (extend-protocol Lowerable
   String
     (lower [s] 
       (.toLowerCase s)))

 (println (lower "HELLO"))

my question is:

  1. besides from performance differences (it is said that Clojure is highly optimized in this regard) - is there a semantic difference between the two approaches?
  2. besides the clumsy syntax, is there anything logically wrong with the Groovy approach?

Disclaimer: I am a complete Clojure newbie!

2
I find this question quite interesting. Could you add the Clojure equivalent for the non Clojure-enlightened of us, and for syntax/brevity comparisons' sake? :Depidemian
sorry, I'd like to, it is just that I can't. On the site clojure.org/protocols there is a quite similar sample, but right now I cannot test the equivalent code, therefore I do not want to post some code which might not work. Sadly, there is no web console like the Groovy web console (there is one on try-clojure.org, but I am even failing with pasting in newline separated lines of code).Ice09
Maybe try with ideone?epidemian
Thanks for the tip, it really works, at least when adding the println statement. I used mikera's version.Ice09

2 Answers

10
votes

Here's the rough equivalent Clojure code using protocols:

(defprotocol Lowerable
  (lower [x]))

(extend-protocol Lowerable
  String
    (lower [s] 
      (.toLowerCase s)))

(lower "HELLO")
=> "hello"

The key distinctions to note about Clojure protocols (which I believe make it distinctive from the Groovy categories version)

  • The Clojure protocol definition does not contain any implementation (it's more like an interface in this respect). The implementation is provided separately: you can extend the Lowerable protocol to as many different classes as you like without needing to make any alterations to either the classes themselves or the protocol definition. For example, you could define lower to work on a Rope.
  • Your Groovy category above is specialised for Strings - this is not the case with Clojure protocols. In this example, the Clojure protocol "Lowerable" is defined without saying anything about the type of arguments.
  • lower is a proper first class function. So you can use it to build higher order abstractions (via higher order functions) that in turn will accept any arguments to which the Lowerable protocol has been extended.
  • Clojure protocols are heavily optimised since they are designed to exploit the fast method dispatch of the JVM. They therefore get compiled down to very efficient code (there is no dynamic object examination or reflection required)

Clojure protocols are actually a fairly unique solution to the Expression Problem (linked video is pretty interesting). I think the closest equivalent to Clojure protocols in another language is actually Haskell type classes. Even then it's a bit of a stretch since Haskell is statically typed and Clojure is dynamically typed....

5
votes

The Clojure feature he's referring to looks like:

(defprotocol StringMunging
  (lower [this]))

(extend-protocol StringMunging
  String
  (lower [this]
    (.toLowerCase this))

  clojure.lang.Keyword
  (lower [this]
    (keyword (lower (name this)))))

user> (lower "TeSt")
"test"
user> (lower :TeSt)
:test

Implementations can be added for any type at any time - there's no need for the two implementations I wrote to cooperate in any way.

However, I don't understand the Groovy well enough to make any substantive comments on the question itself; I can only help describe the question's Clojure side.