Polymorphism in Clojure

“polymorphism is a programming language feature that allows values of different data types to be handled using a uniform interface” -Wikipedia

In most Object Oriented programming languages, polymorphism is tied to inheritance. In Clojure however, the concept of concrete inheritance is not built into the language. So, when I was first learning Clojure, it was hard for me to use my previous knowledge of polymorphism in the functional world. No need to fear, Clojure provides great methods for achieving polymorphism without using concrete inheritance.

As an example, this is a simple function that takes basic Clojure data and converts it to JSON.

 1 (defn convert [data]
 2   (cond
 3     (nil? data)
 4       "null"
 5     (string? data)
 6       (str "\"" data "\"")
 7     (keyword? data)
 8       (convert (name data))
 9     :else
10       (str data)))

This works, but it is not polymorphic. So let's take a look at a few ways to improve this.

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.

 1 (defmulti convert class)
 2 
 3 (defmethod convert clojure.lang.Keyword [data]
 4   (convert (name data)))
 5 
 6 (defmethod convert java.lang.String [data]
 7   (str "\"" data "\""))
 8 
 9 (defmethod convert nil [data]
10   "null")
11 
12 (defmethod convert :default [data]
13   (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.

1 (defmethod convert clojure.lang.PersistentVector [data]
2   (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,

1 user=> (derive ::rect ::shape)
2 nil
3 user=> (derive ::circle ::shape)
4 nil
5 user=> (isa? ::circle ::shape)
6 true
7 user=> (isa? ::rect ::shape)
8 true

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

1 (derive clojure.lang.PersistentVector ::collection)
2 
3 (defmethod convert ::collection [data]
4   (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.

1 (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

1 (defmethod convert clojure.lang.Sequential [data]
2   (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.

Protocols

Another way to improve our original switch statement is though Protocols. Protocols will be a little more familiar for those coming from Object Oriented languages, as they are very similar to interfaces. Let's look at our original function refactored to use a Protocol.

 1 (defprotocol JSON
 2   (to-json [this]))
 3 
 4 (extend-protocol JSON
 5   java.lang.Boolean
 6     (to-json [this]
 7       (str this))
 8 
 9   java.lang.Long
10     (to-json [this]
11       (str this))
12 
13   java.lang.Double
14     (to-json [this]
15       (str this))
16 
17   java.lang.String
18     (to-json [this]
19       (str "\"" this "\""))
20 
21   clojure.lang.Keyword
22     (to-json [this]
23       (to-json (name this)))
24 
25   nil
26     (to-json [this]
27       "null"))

In this example, we define a Protocol, JSON, which has one method, to-json. Then we use the helper macro extend-protocol to extend all of our types at once. Now, we can simply call the to-json method directly on the data types themselves rather than through a conversion function.

1 user=> (to-json "1")
2 "\"1\""
3 user=> (to-json 1)
4 "1"

In the same way that interfaces are used in conjuction with classes in Java and C#, protocols, in conjunction with deftype or defrecord, can be used to define an explicit API that a type implements. For example,

 1 (defprotocol Dog
 2   (sit [this])
 3   (bark [this])
 4   (eat [this]))
 5 
 6 (deftype Terrier []
 7   Dog
 8   (sit [this]
 9     (prn "sitting"))
10   (bark [this]
11     (prn "woof!"))
12   (eat [this]
13     (prn "nom nom nom!")))
14 
15   (defn new-terrier []
16     (Terrier.))

Using this method, we can kind of mimic objects. Terrier can be instantiated and used just like any other object.

 1 user=> (def terrier (new-terrier))
 2 #'user/terrier
 3 user=> (bark terrier)
 4 "woof!"
 5 nil
 6 user=> (sit terrier)
 7 "sitting"
 8 nil
 9 user=> (eat terrier)
10 "nom nom nom!"
11 nil

Even though I don’t like mimicking objects in Clojure, sometimes it is necessary in order to use Java interfaces and objects, which you can learn more about here.

As with multimethods, type definitions do not have to be defined in the same file as their protocol definition, allowing us to implement protocols defined in other libraries.

As seen in the Dog/Terrier example, Protocols are great for encapsulating a group of related methods into one polymorphic package. However, this method of polymorphism should be used sparingly. Protocols are often abused to mimick an Object Orient approach because it is a familiar solution. However, if you do this, you will often miss out on the simplest solution, pure functions.

Functions as parameters

This is simplest form of polymorphism in Clojure. By accepting a function as a parameter, the given function proide multiple solutions with one interface (the parameters). Instead of refactoring our original function, let's consider a context in which it is commonly used. Web servers often need to convert data structures into JSON before returning them to clients. So let's consider a resource controller, which has an action to create a User given the request parameters.

1 (defn create [params]
2   (let [user (build-user params)
3         user (save user)]
4         (convert-to-json user)))

This function simply takes the request params, builds a user model out of them, saves them to the database and returns the result as JSON. We can make this controller polymorphic by accepting the JSON converter function as a parameter to the create function instead of calling it directly.

1 (defn create [params converter]
2   (let [user (build-user params)
3         user (save user)]
4     (converter user)))

Now the create function doesn’t have to know anything about the structure of the data that is returned to the user. The given converter function is polymorphic, meaning that it can convert the data into whatever format the user requests, instead of just JSON. So if the user requests XML or HTML instead of JSON, we don’t need to alter our create function; we simply need to pass in a different converter.

Conclusion

Using functions as parameters is by far my favorite form of polymorphism in Clojure. It is this simplest and most consise of all three methods; I try to use this method whenever possible. However, Multimethods and Protocols are equally powerful and provide useful ways in which to design polymorphic systems without concrete inheritence. With these methods in your tool belt, you don't have to feel as lost as I did when coming into the world of Clojure.

Myles Megyesi, Software Craftsman

Myles Megyesi loves design patterns, functional programming, and popcorn.