45. À la Carte Polymorphism, part 1
FreebiePublished 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.