Multimethods in Clojure

(written by lawrence krubner, however indented passages are often quotes). You can contact lawrence at: lawrence@krubner.com, or follow me on Twitter.

I’ve only recently discovered multimethods, but now I’ve come round to the idea that one should never use (cond) in Clojure. Rather, any time you have a complicated nexted (ifs) or a big (cond) it should all be replaced by multimethods:

A Clojure multimethods is a combination of a dispatch function and one or more methods, each defining its own dispatch value. The dispatching function is called first, and returns a dispatch value. This value is then matched to the correct method. Lets take a look at our previous example refactored into a multimethod.

(defmulti convert class)

(defmethod convert clojure.lang.Keyword [data]
(convert (name data)))

(defmethod convert java.lang.String [data]
(str “\”” data “\””))

(defmethod convert nil [data]
“null”)

(defmethod convert :default [data]
(str data))

Awesome! We have our first polymorphic solution. Now we can add more data types without altering the existing functions. Let’s add a method for vectors as well.

(defmethod convert clojure.lang.PersistentVector [data]
(str “[” (join “, ” (map convert data)) “]”))

Now we can also convert vectors into JSON.

There is another feature of multimethods that we can use to extend this solution further. Multimethods actually use the isa? function instead of the = function to match dispatch values to the correct method. This yields a very important feature, hierarchies. Let’s open up the REPL and take a look at the classic Shape example,

user=> (derive ::rect ::shape)
nil
user=> (derive ::circle ::shape)
nil
user=> (isa? ::circle ::shape)
true
user=> (isa? ::rect ::shape)
true

Now let’s see if we can apply this hierarchy system to our previous vectors method.

(derive clojure.lang.PersistentVector ::collection)

(defmethod convert ::collection [data]
(str “[” (join “, ” (map convert data)) “]”))

With this hierarchy, any type that matches ::collection will dispatch to the same method as vectors. Since there is no there is no difference between a list and vector in JSON, we can convert them to JSON in the same way, so we simply make PersistentList derive from ::collection as well.

(derive clojure.lang.PersistentList ::collection)

We were able to extend the multimethod to handle Lists with one line!

Its also worth noting that we could implement the same functionality without introducing hierarchies here. List and vectors already share many common interfaces, so we could use one of those instead. We simply replace our vector method with this

(defmethod convert clojure.lang.Sequential [data]
(str “[” (join “, ” (map convert data)) “]”))

Now we’re able to convert vectors and lists without using hierarchies.

Another great feature that is hard to demonstrate here is that the methods do not have to be defined in the same file as their dispatch function. This allows us to extend the functionality of multimethods that are defined elsewhere in the system or even in a 3rd party library.

Multimethods can be very useful in situations where you cannot change the clients of a function. For instance, if there are thirty other functions using the convert function, it is hard to change. However, we can refactor into a multimethod without changing any of the clients. This allows us to refactor safely without affecting any clients.

In my experience, multimethods are best used in cases where you only need to define one polymorphic function. When multimethods are used to define a group of polymorphic methods, this solution can get a little messy. However, Clojure provides great facilities for this as well, namely Protocols.

Myles Megyesi also says his favorite form of polymorphism in Clojure is simply handing in functions as an argument. That does have its place, for sure, but it can also be very sloppy. Multimethods offer some amazing organization: you can put :pre and :post assertions on your dispatch function and thus enforce a contract on dozens of methods, having only written the contract once. I find that impressive.

Post external references

  1. 1
    http://blog.8thlight.com/myles-megyesi/2012/04/26/polymorphism-in-clojure.html
Source