Chapter 5. Multimethods and Protocols
We now have a better understanding of how Clojure works; we understand how to perform simple operations with immutable data structures but we are missing some features that could make our lives much easier.
If you have been a Java programmer for a while, you are probably thinking about polymorphism and its particular flavor in Java.
Polymorphism is one of the concepts that enable us to reuse a code. It gives us the ability to interact with different objects with the same API.
Clojure has a powerful polymorphism paradigm that allows us to write simple code, create code that interacts with types that don't exist yet, and extend code in ways it wasn't devised for when a programmer wrote it.
To help us with polymorphism in Clojure, we have two important concepts that we will cover in this chapter:
Each of them has its own use cases and things it is best at; we will look into them in the next section.
We will learn each of these different concepts...
Java uses polymorphism heavily, its collection API is based on it. Probably the best examples of polymorphism in Java are the following classes:
java.util.List
java.util.Map
java.util.Set
We know that depending on our use case we should use a particular implementation of these data structures.
If we prefer to use an ordered Set, we might use a TreeSet.
If we need a Map in a concurrent environment, we will use a java.util.concurrent.ConcurrentHashMap
.
The beautiful thing is that you can write your code using the java.util.Map
and java.util.Set
interfaces and if you need to change to another type of Set or Map, because the conditions have changed or someone has created a better version of the collection for you, you don't need to change any code!
Lets look at a very simple example of polymorphism in Java.
Imagine that you have a Shapes hierarchy; it might look similar to the following code:
Multimethods are similar to interfaces, they allow you to write a common contract and then a family of functions can fulfill that interface with a specific implementation.
They are extremely flexible, as you will see they grant you a very fine control over what function is going to get invoked for a specific data object.
Multimethods consist of three parts:
One of the most interesting features of multimethods is that you can implement new functions for already existing types without having to write wrappers around your currently existing object.
The multimethod declaration works the same way as the interface; you define a common contract for the polymorphic function, as shown:
The defmulti
macro defines the contract for your multimethod, it consists of:
Multimethods are just one of the options for polymorphism you have in Clojure, there are other ways to implement polymorphic functions.
Protocols are a little easier to understand and they might feel more similar to Java interfaces.
Lets try to define our shape program using protocols:
Here, we have defined a protocol and it is called shaped and everything that implements this protocol must implement the following two functions: perimeter
and area
.
There are a number of ways to implement a protocol; one interesting feature is that you can even extend Java classes to implement a protocol without an access to the Java source and without having to recompile anything.
Let's start by creating a record that implements the type.
Records work exactly like maps, but they are much faster if you stick...
Now we understand Clojure's way a little bit better and we have a better grasp of what to look for when we need polymorphism. We understand that when needing a polymorphic function we have several options:
We can implement multimethods if we need a highly customized dispatching mechanism
We can implement multimethods if we need to define a complex inheritance structure
We can implement a protocol and define a custom type that implements that protocol
We can define a protocol and extend existing Java or Clojure types with our custom functions for each type
Polymorphism in Clojure is very powerful. It allows you to extend the functionality of Clojure or Java types that already exist; it feels like adding methods to an interface. The best thing about it is that you don't need to redefine or recompile anything.
In the next chapter, we will talk about concurrency—one of the key features of Clojure. We will learn about the idea of what the identity and values are and how those key concepts make...