Photo by Emile Perron on Unsplash

Polymorphism with Clojure Protocols

Ilan Uzan
6 min readApr 14, 2021

--

In my previous post I wrote about how we can implement polymorphism in Clojure with multimethods. I showed that it’s actually such a powerful tool, that we can do with it a lot more then just modelling class hierarchy.

However, we shouldn’t use a sledgehammer to crack a nut, or in other words — while we can implement traditional polymorphism (single type based dispatch) with multimethods, it doesn’t mean we should. There’s a simpler (and more performant — see here) way in Clojure — Protocols.

The Basics

Protocols replace what in an OOP language we know as interfaces. Let’s see how we define a protocol and what we can do with it:

(defprotocol Dog
(bark [dog] [dog x] "dog barks")
(eat [dog] [dog] "dog eats"))
  1. We defined a protocol called Dog with the defprotocol macro, that has 2 methods — bark & eat.
  2. Each method expects at least one argument — the entity it operates on. The type of that entity is how Clojure understands what implementation to use — single type based dispatch.
  3. We defined 2 arities (the number of arguments the method takes) for the first method bark.

Now we want to define a type that implements that protocol— we do that by using the defrecord / deftype macros***:

(defrecord Pitbull [name]
Dog
(bark [dog] (println (str name " goes 'RRRooof! Woof woof!'")))
(bark [dog x] (repeatedly x #(bark dog)))
(eat [dog] (println (str name " eats and grunts"))))

***There are several differences between records and types in Clojure, in this post I’ll be using records exclusively, and will refer to them simply as types. You can read more about records and types in here.

We invoke the bark method on a Pitbull record:

(bark (Pitbull. "Pete"))
Pete goes 'RRRooof! Woof woof!'

Now, let’s dive a little deeper into the similarities and the differences between protocols and interfaces.

Java Interfaces vs. Clojure Protocols

The Similarities

Like Java interfaces (at least until Java 8 introduced default methods in interfaces), Clojure protocols provide no methods implementation, only specification.

Also, much like a Java class can implement multiple interfaces, a Clojure type can implement multiple protocols.

The Differences

The doc states it well:

Which interfaces are implemented is a design-time choice of the type author, cannot be extended later

Let’s say we want our Pitbull type to implement an additional protocol, Playful. If we were in Java and wanted the Pitbull class to implement the Playful interface as well, it would require changing the Pitbull class definition.

In order to have the class Pitbull class implement the Playful interface, we had to change the existing class. In Clojure it’s not necessary:

(defprotocol Playful
(play [dog] "plays"))
(extend-type Pitbull
Playful
(play [e] (println (str name " is chasing the ball"))))
(play (Pitbull. "Pete"))
Pete is chasing the ball

At this point you might say — well, it isn’t such a big deal to change the Pitbull class to implement an additional interface. Well, that maybe true when the class is maintained by you or someone in your team, but what if it’s a class in an imaginary 3rd-party pets library? In that case, you have no control of the class definition — the best you can do is sub-class Pitbull, and have that class implement Playful.

The fact that Clojure allows us to independently extend types and protocols is how Clojure basically avoids the “expression problem” —we are able to add new functions that can operate across different types (normally harder in OOP languages) and new types that a set of functions can operate on (normally harder in functional languages) easily.

Let’s understand it a bit better with an example.

Solving the Expression Problem

As a first step, I want to model the following in Clojure:

  1. A Bird protocol that specifies 2 methods: fly & nest.
  2. Two types: dove & raven that implement this protocol.

And just like we did before:

(defprotocol Bird
(fly [bird] "flying")
(nest [bird] "nesting"))
(defrecord Dove []
Bird
(fly [bird] "flying and bringing peace all around")
(nest [bird] "in my nest with future peace bringers"))
(defrecord Raven []
Bird
(fly [bird] "Once upon a midnight dreary, I flew weak and weary")
(nest [bird] "While I nodded, suddenly my nest was tapping"))

Now, let’s see how we avoid the expression problem — we want to:

  1. Have the birdperson type (some pre-existing type) implement the bird protocol as well.
  2. Have the Dove, Raven & Birdperson types implement an additional musician protocol, that has a play-music method.

First, let’s have birdperson type implement the bird protocol:

(extend-type Birdperson
Bird
(fly [bird] "flying to my wedding with Tammy")
(nest [bird] "nesting baby humans and birds alike"))

We use the extend-type macro (expands to multiple expand function calls, one for each protocol we wish the type to implement), that is being used to add multiple protocols to same type at once — here we just need to add the Musician protocol.

Now, we want the 3 types to implement an additional musician protocol:

(extend-protocol Musician
Dove
(play-music [m] "making dove sounds incoherently but peacefully")
Raven
(play-music [m] "rapping in bleak December")
Birdperson
(play-music [m] "sings an unjustified love song for Tammy"))

We use the extend-protocol macro (expands to multiple extend-type macros, one for each type) to implement the Musician protocol on the 3 data types — you can find the documentation on the extend function and its 2 helper macros in here.

Let’s see that it works:

(println (fly (Birdperson.))) => flying to my wedding with Tammy
(println (play-music (Dove.))) => making dove sounds incoherently but peacefully
(println (play-music (Birdperson.))) => sings an unjustified love song for Tammy

Using extend-type & extend-protool we were able to have a single type implement multiple protocols and have a single protocol being implemented by multiple types — each in a single statement.

Default Protocol Implementations

Unlike multimethods (and Java interfaces until Java 8), protocols don’t provide default implementations. There are several ways to overcome this:

Have java.lang.Object Implement the Protocol

(extend-protocol Musician
java.lang.Object
(play-music [o] "anyone can make music"))
(println (play-music {:x 1})) => anyone can make music

Straight-forward solution —the play-music function now can operate on any data type, because every data type in Clojure is essentially derived from java.lang.Object.

We face here the issue of prioritization of implementations —the most specialized implementation is being used when there are 2 available implementations, but what happens when there are 2 implsee you inementations on the same level of specialization?

From a quick check I did on the IEditableCollection and IMapIterable interfaces that are in the same level of specialization in the PersistentArrayMap class hierarchy (the type of {:a 1})it arbitrarily takes one of the implementations (unlike Java that fails to compile in that situation):

(extend-protocol Musician
clojure.lang.IEditableCollection
(play-music [o] "music editable")
clojure.lang.IMapIterable
(play-music [o] "music iterable"))
(println (play-music {:a 1})) => music serializer

Not an ideal solution, but might be sufficient for most cases. In multimethods this is solved simply by the prefer-method function.

Data Type Check with Satisfies?

(defprotocol Painter
(paint [this]))
(defrecord Impressionist []
Painter
(paint [p] "impressionist painting"))
(defn paint
[x]
(if (satisfies? Painter x)
(paint x)
"default painting"))

The main problem here is that this becomes the default implementation for all data types. In the previous solution we can extend a more specialized type than Object, allowing us more flexibility in choosing what data types we allow the function to operate on.

Regular Function that Uses Protocol Method

This fits in a specific usecase — but when it’s applicable it’s definitely a good one. Say we have a protocol with 2 methods — the first one uses the second one, and we want to provide a default implementation for the first one.

In that case, we just make the default method a a regular function, and use the method that is unique to the protocol in it:

(defprotocol Saving
(get-payload [this] "saving obj"))
(defn save [obj] (get-payload obj))(defrecord DBObject []
Saving
(get-payload [this] "generating a db object"))
(println (save (DBObject.))) => generating a db object

And then each data type that wants to be “saved” can simply implement the Saving protocol.

Photo by Asique Alam on Unsplash

Conclusion

Well, that was a longer post than I expected it to be. In this post I explained Clojure Protocols in depth. We saw how Protocols are similar and different from Java Interfaces, how Clojure solves the “expression problem” with protocols, and we also talked about how we can achieve default implementation for Protocols’ methods.

All of the code shown in this post is available here. Hope you enjoyed the post, and looking forward to see you in the next one.

--

--