Clojure and OO part 2 defrecord

In the last post, I talked about how to create interfaces/protocols in Clojure with definterface and defprotocol, and from this post on, I will start talking about various ways in Clojure to implement them.

And I will start with the simpler ones, such as defrecord.

What is it?

One can use defrecord to make a solid class implementing one or more interface/protocols in Clojure. As the name suggests, it’s mainly used for defining record types, as in types that are normally used to store data.

Let’s look at an example:

(defrecord Cat [name age breed])

Uh…that’s a bit simple you say? Yes, you’ve got it! It’s supposed to be simple!

It defines a java class that have three fields in it - name, age and address. That’s it. Piece of cake. Now here’s an important bit: All the classes generated by defrecord implement the IPersistentMap interface. And what does that mean? It means you can treat these fields like how you normally access the corresponding value of a keyword in a map in Clojure.

Let’s make a new instance of our shiny new Cat class:

(def sherlock (->Cat "Sherlock" 3 "Russian Blue"))

“Wait wait wait, what is ->Cat?? I never defined such a thing!” I can already hear our keen readers interrupting me here. Well, you did, and you just didn’t know you did. You did back when you called that defrecord in our first code excerpt. Clojure will automatically define this handy function for you to use to create new instances of your record types. (And yes, before you ask, Sherlock is one of my endearing cats at home.)

And Clojure has another trick up its sleeve - there is also another defined function named map->Cat. The name should already tell you what it does - it turns a map into an instance of our new Cat type.

Here is map->Cat doing the same thing as ->Cat:

(def sherlock (map->Cat {:name "Sherlock" :age 3 :breed "Russian Blue"}))

It’s a bit more verbose, but it’s a lot clearer what you are passing to each field, basically the difference between ordered parameters and named parameters. You can take your own pick, I personally prefer named parameters.

OK. Now we have our cat Sherlock. How do you get its age again? As I said above, you can treat these record type instances like how you normally treat maps in Clojure:

(println (:age sherlock))
; We might want to update his age when he grows older...
(def older-sherlock (update sherlock :age inc))

Again, very simple…

But, is that all defrecord can do? No, my friend. These record types can also implement interfaces, remember? They can’t have their own methods, but they can, and have to implement all the methods they inherit from various interfaces.

Let’s make the famous Animal interface and a fun LaserChaser interface:

(defprotocol Animal
  (make-noise [this] "Make the animal noise"))

(defprotocol LaserChaser
  (chase-laser [this] "Chase the laser!"))

Now let’s have an upgraded Cat record type that implements these two interfaces.

(defrecord UpgradedCat
  [cat-name age breed]
  Animal
  (make-noise [this] (println "meow"))
  LaserChaser
  (chase-laser [this] (println cat-name "made a pounce for the laser dot!")))
  
(def upgraded-sherlock (map->UpgradedCat {:cat-name "Sherlock" :age 3 :breed "Russian Blue"}))
(make-noise upgraded-sherlock)
(chase-laser upgraded-sherlock)

Now remember, you can get record types to implement any interfaces, not just Clojure defined ones.

And I think that’s about it for defrecord. Oh, and one last thing, you cannot use defrecord to extend solid classes. We will eventually get around to that, probably towards the end of this series. For now, it’s wrap-up time! Go watch TV or something, or hopefully you may have been excited about this enough to go write some Clojure!

P.S. defrecord @ ClojureDocs

Published: November 07 2015

blog comments powered by Disqus