Implementing Clojure Protocols
Protocols are Clojure’s way of defining Java interfaces. Here, I will show how to define protocols and how to implement them.
How to Define a Protocol
(defprotocol Drives
"a protocol for driving"
(drive [this throttle]))
Now we’ve got a Java interface named Drives
with one (polymorphic) public function drive
to be implemented by deftype or defrecord. You could also implement the protocol in Java with a little interop magic. I won’t get into this, however.
The drive
function is polymorphic in that it will dispatch on the type of the first argument to the function. This is why every protocol function must take at least one argument. You can think of the first argument as always being this
.
Implement a Clojure Protocol
Let’s implement the Drives
protocol and create a concrete Car
class.
(defrecord Car [top-speed]
Drives
(drive [_ throttle] (str "Spin wheels " (* top-speed throttle))))
Java equivalent:
class Car implements Drives {
private float top_speed;
public Car(float top_speed) {
this.top_speed = top_speed;
}
public String drive(float throttle) {
return "Spin wheels " + Float.toString(this.top_speed * throttle);
}
}
Now we’ve got a concrete implementation of Drives
named Car
. As you can see, we implemented the drive
function.
What’s the _
parameter to drive
you may ask? It essentially means “I don’t care about this value you’re passing in.” It is convention to use _
as the parameter name for values in which your function doesn’t use. In this case, it would be this
, the passed in Car
instance.
Let’s drive some cars.
(def ferrari (Car. 200.0))
(def honda (Car. 80.0))
(drive ferrari 0.5) ;;=> Spin wheels 100.0
(drive honda 1.0) ;;=> Spin wheels 80.0
With this very scientific and accurate physics model, a Ferrari applying only 50% throttle will drive faster than a Honda with the pedal to the floor.
So what’d we do? First, we instantiated two instances of Car
by passing in the top-speed
value to the constructor. To call one of the functions from the protocol, you do so just as you would any other clojure function. Passing in the object as the first argument.
Java equivalent:
Drives ferrari = new Car(200.0);
Drives honda = new Car(80.0);
ferrari.drive(0.5);
honda.drive(1.0);
Polymorphic Function Dispatch
So we’ve implemented Car
and shown that (drive ferrari 0.5)
works as you would expect. What about another concrete class Boat
which also implements Drives
.
(defrecord Boat [top-speed]
Drives
(drive [_ throttle] (str "Spin propeller " (* top-speed throttle))))
(def chris-craft (Boat. 70.0))
(drive chris-craft 0.8) ;;=> Spin propeller 56.0
The drive
function dispatched to the implementation defined by Boat
.
Using Records and Types Elsewhere
Under the covers, the record is essentially a map. This means you can use it just as you would any other map-like data structure.
Get the top-speed
of the ferrari.
(:top-speed ferrari) ;;=> 200.0
Create a blue-honda
.
(def blue-honda (assoc honda :color "blue"))
;;=> Car{:top-speed 80.0, :color "blue"}