Saturday, May 31, 2014

Clojure protocols example 2: Extending existing classes

In my past post "Clojure protocols example" I gave an example about using protocols on new types. But protocols also can be used to extend existing types.

A real world example of this can be found in this library, used to generate message digest (md5, etc) for different input types.

More specifically this file.

Here we see existing data types (Collection, String, File, etc) are extended to implement Digestible protocol. So we can call digest on them - a method which they don't support natively.

This is a different, simplified version, with just what we need to understand:
(defprotocol Digestible
  (digest [digestible]))

(extend-protocol Digestible

  (digest [digestible]
    (str "vector-md5-" digestible))

  (digest [digestible]
    (str "map-md5-" digestible))

  (digest [digestible]
    (str "string-md5-" digestible)))

(digest {:a 1 :b 2}) ;map-md5-{:a 1 :b 2} 
(digest [1 2 3]) ;vector-md5-[1 2 3]
(digest "hellooo") ;string-md5-"hellooo"

In this case, we could say, explaining it in object oriented fashion, that we are extending the existing map, vector and string classes in Clojure to support the digest function. A more functional explanation, and thus suitable for this context, would be that we are defining a function, which will execute implementation according to the type of the parameter.
Either way, we call digest with these types, and get the correct hash. In this example, probably needless to say, for demonstrative purposes, we don't return a hash but a text that helps us to see which method was called.

Note that in this example I'm using "digestible" instead of "this" to name the parameter (contrary to the first example I posted about protocols). There's no reason for this, besides I consider "this" maybe to be too engrained into object oriented world and misleading in this context. I may change the other post to use "data-provider" instead of "this". The source of the GitHub project I mentioned uses "message". I consider "digestible" better, since if we pass anything here that's not "digestible", we will get an error. It seems there is no (well known) convention about the naming of this parameter yet, since all the examples I have found use something different.

On a different note, this functionality can also be achieved using multimethods. This is the multimethod version of our example:

(defmulti digest class)

(defmethod digest clojure.lang.PersistentVector [digestible]
  (str "vector-md5-" digestible))

(defmethod digest clojure.lang.PersistentArrayMap [digestible]
  (str "map-md5-" digestible))

(defmethod digest java.lang.String [digestible]
  (str "string-md5-" digestible))

(digest {:a 1 :b 2})
(digest [1 2 3])
(digest "hellooo")

More info on when to use protocol or multimethod e.g. here.

No comments:

Post a Comment