45. À la Carte Polymorphism, part 1

Freebie

Published 30 October 18

Clojure provides polymorphism through protocols and multimethods. Protocols were covered in depth in episode 24. This episode provides a brief recap, then looks at multimethod basics. If you are already familiar with multimethods then you might want to skip to the second part, which covers some of lesser known aspects.

Clojure is sometimes described as having “à la carte polymorphism”, a phrase which can be puzzling, even to those who do have a firm grasp of French and Greek. That it’s no false claim though will become clear in this episode.

This episode is intended as an introduction. If you feel you’re already well versed in protocols and multimethods then feel free to skip to part two, where I’ll go over some of the more advanced features and uses.

“Poly-morphic”, from Greek, means “having many forms”. It’s a term programmers borrowed from biology, where it’s used to describe species where within the same population you have clearly discernable subgroups, for example the two-spotted ladybird beetle is most commonly red with two black spots but can also appear in other “morphs”, the main one being black with yellow spots.

In programming polymorphism means having a single interface with multiple implementations. Which implementation to run in a given instance is chosen based on attributes of the values supplied to the function, most commonly their class or type.

(conj #{} :x)
;; => #{:x}

(conj [] :x)
;; => [:x]

(conj {} [:x :y])
;; => {:x :y}

“à la carte” is the opposite of a set menu, it means you can order individual dishes and sides, instead of getting the package deal. So “à la carte polymorphism” suggests that polymorphism in Clojure is more fine grained than in other languages, that you can mix and match the various pieces, instead of having to settle for the Sunday Special.

Instead of dwelling on the theory, let’s look at the features Clojure provides, and how they differ from other languages.

Clojure provides two main ways to achieve polymorphism: protocols and multimethods. The main focus of this episode is on multimethods. I discussed protocols in depth in episode 24, but I will recap them briefly to set the stage.

As an example consider this protocol called “Datelike”. It defines the signature of three functions, year, month, and day. Each function takes single argument, a “datelike”, and returns an integer.

The protocol only defines the signature of these functions, not their implementation. You can add multiple implementations later on, and because of that these functions are caled “polymorphic”. Polymorphic functions are also known as “methods”.

(defprotocol Datelike
  (year [d] "return the year (common era)")
  (month [d] "return the month (1-12)")
  (day [d] "return the day of the month (1-31)"))

year, month, and day don’t have any implementations yet, but they form a kind of contract. If I get a Datelike value in my program, then I know I can call year, month, or day on it, and get a number back.

So for instance I could write this function, which calculates the days since new year. Don’t worry too much about the implementation, in fact it’s not unlikely I got something wrong, but the point is that by relying on the contract provided by the year, month and day functions, I can write this function and make sure it will work with any Datelike value.

(defn days-since-new-year [datelike]
  (let [y (year datelike)
        m (month datelike)
        d (day datelike)
        leap? (and (= 0 (mod y 4))
                   (or (not= 0 (mod y 100))
                       (= 0 (mod y 400))))]
    (cond-> (apply + (take m [d 31 28 31 30 31 30 31 31 30 31 30 31]))
      (and leap? (> m 2)) inc)))

But what’s a Datelike? Somewhat cirularly, it’s anything that implements the Datelike protocol. You can implement a protocol by using defrecord. This creates a custom data structure type with named fields. Inside the defrecord you put the name of the protocol, followed by the implementations of its functions. Note that inside a defrecord you can access the fields of the record directly, making these definitions very short.

(defrecord NaiveDate [y m d]
  Datelike
  (year [this] y)
  (month [this] m)
  (day [this] d))

With that record defined I can now create a NaiveDate, and then call the year, month, or day methods on it. And since a NaiveDate is a Datelike, I can also use it with days-since-new-year. So far so good.

(def d (->NaiveDate 2018 10 22))

(year d)  ;;=> 2018
(month d) ;;=> 10
(day d)   ;;=> 22

(days-since-new-year d) ;;=> 294

When you call year on something that isn’t a Datelike, you get an exception. Clojure tried to find an implementation of Datelike’s year method for java.util.Date, but didn’t find one, so it gives up.

(year (java.util.Date.))
;;=> No implementation of method: :year of protocol: #'user/Datelike found for class: java.util.Date

To make this work we need to extend java.util.Date to implement the Datelike protocol. In most languages including Java you can’t just extend an existing type like this. That you are able to do this is one of the great features of Clojure’s protocols, and something that sets them apart from typical interface implementation or type inheritance.

(extend-type java.util.Date
  Datelike
  (year [this] (+ 1900 (.getYear this)))
  (month [this] (inc (.getMonth this)))
  (day [this] (.getDate this)))

Now the methods defined in Datelike can also be used on java.util.Date instances. You can imagine adding more implementations, say for java.time.Instant, or even for integers, perhaps treating them as UNIX timestamps.

(year (java.util.Date.))  ;;=> 2018
(month (java.util.Date.)) ;;=> 10
(day (java.util.Date.))   ;;=> 22

Let’s try again to call the Datelike methods on a type we haven’t provided implementations for: java.sql.Date. Perhaps suprisingly we don’t get an error this time. The reason is that java.sql.Date is derived from java.util.Date. In the hierarchy of Java types a java.sql.Date “is a” java.util.Date. Clojure will try to find an implementation for java.sql.Date, but failing that it will look up the parent class, java.util.Date.

(Note that none of these examples are intended to show to actually deal with dates in your application. The classes shown here have known issues, and you should stick to the java.time API where possible.)

Finding the right function implementation to call is known as “dispatching”. Protocols dispatch based on the object’s class, keeping in mind the hierarchy of parent and child classes. This is something the JVM knows how to do natively, making it very fast.

Quite often dispatching based on the class is exactly what you want anyway, so use protocols where applicable. You can learn about them in-depth in episode 24.

So, in summary, protocols provide: a dispatch mechanism to pick a function implementation, this dispatching is based on an object’s type, and, types form a hierarchy, which is condsidered by the dispatching mechanism to find fallbacks.

Let’s see if we can pry those three things apart.

(def s (java.sql.Date. 1540240505000))

(year s)  ;;=> 2018
(month s) ;;=> 11
(day s)   ;;=> 22

(supers java.sql.Date)
;; => #{java.io.Serializable java.util.Date java.lang.Cloneable java.lang.Object
;;      java.lang.Comparable}

Say you’re writing a game, you have various in-game items like doors, walls, or magical potions, which you represent as pure Clojure data, in this case as maps.

{:item/type :door, :door/open? true}
{:item/type :wall}
{:item/type :magical-potion}

You need to be able to tell if an item blocks the player from moving or not, so you want to implement a obstacle? function which returns true or false for a given item. You could do this with a simple case statement, but this means your obstacle? function needs to know ahead of time about all items in the game.

You’ll likely end up with multiple functions like this, checking if an item can be picked up, or destroyed, or sold, each of them with an ever growing case statement inside. That’s not great.

(defn obstacle? [item]
  (case (:item/type item)
    :door (not (:door/open? item))
    :wall false
    true))

How to solve this? Each item is just a Clojure map, so in that sense they all have the same “type”, so protocols won’t do you much good, but each item also stores its type inside the map, what if you could dispatch on that? Multimethods to the rescue!

You create multimethods using two macros, defmulti and defmethod. defmulti takes the name of the method and a dispatch function. This function shows that I want to dispatch on the :item/type value in the item map. You can optionally add a docstring and a map with metadata.

When calling obstacle? Clojure will first pass any arguments to the dispatch function. In this case we call obstacle? with a single item argument, so that’s what the dispatch function receives.

(defmulti obstacle?
  "Does this item block the player's path?"
  {:added "1.0"}
  (fn [item] (:item/type item)))

The return value from the dispatch function is then used to find a suitable implementation. These implementations are provided with defmethod. After the method name you put the dispatch value, and then the argument vector and function body.

(defmethod obstacle? :door [item]
  (not (:door/open? item)))

(defmethod obstacle? :wall [item]
  true)

Astute viewers will have noticed that the dispatch function could actually just be a keyword, which is fairly common.

(defmulti obstacle? :item/type)

What happens when you pass in a value that doesn’t have a matching method? In this case Clojure throws an exception, similar to what happened when calling a protocol method on an unsupported type.

(obstacle? {:item/type :magic-potion})
;;=> java.lang.IllegalArgumentException
;;   No method in multimethod 'obstacle?' for dispatch value: :magic-potion

To prevent this from happening you can implement a fallback implementation. If no matching method is found, then Clojure will look for a method with the dispatch value :default. So if you want items to not be obstacles by default, then you can supply this implementation.

(defmethod obstacle? :default [item]
  false)

This episode has been a recap of protocols and multimethods. For some of you it might not have provided much news, but I wanted to introduce the concept of polymorphism first, and get everyone on the same page, before digging into the more advanced stuff.

In the next episode you’ll learn about extra options for defmulti, clojure’s built-in and custom hierarchies, interesting uses of the type function, and some practical examples of cases where multimethods come in handy.