Clojure has three major features which support object-oriented programming: records, protocols, and multimethods. Records and protocols together are closer to traditional classes like we have in Java and Python. A record is rather like a C struct: you can declare record types with the
defrecord macro, and give a list of fields which are available on that record type. This causes a constructor function to be defined. The fields of a record are accessed just like a regular Clojure map; you can use
get, or the names of the fields as keywords define functions which take the record as argument and return the value of the field. Unlike C structs, you can add fields to a record using Clojure’s
assoc, just like you can with maps.
However, fields defined in the record actually become instance fields of a Java class which represents the record in the generated bytecode. The JVM is optimized around Java classes, so accessing instance fields is very fast. So records are sort of a performance enhancement.
Records are data holders. Since Clojure has first-class functions, you could theoretically have the data a record holds be functions, and then it would sort of feel like a class. But protocols and multimethods are better ways to get behavior on records; they permit polymorphism and limited types of inheritance.
Protocols are somewhat like Java interfaces. They’re lists of function names and signatures, defined with the
defprotocol macro. When you define a record type, you can also have it adhere to a protocol, which is like implementing an interface: you give the bodies of the functions, which all take the record as the first argument. Different record types can adhere to the protocol in different ways, which gives you polymorphism in the same way interfaces do.
Multimethods are apparently a concept which originated in Common Lisp’s CLOS, although I have no familiarity with CLOS. They’re a little tricky to understand, but incredibly powerful.
In an object-oriented language like Java, you can have multiple classes implement the same method. When you call the method on some object, the runtime decides which version of the method to call based on the class of the calling object. Say we have a
Goth class and a
Beatnik class, which both inherit from
Counterculturalist and both implement the method
IPoetic. Given an object
Counterculturalist c, we execute
c.writePoetry(). Then, if
c is an instance of
Goth, we’ll get back something like “The world…is dark. Dark…like the depths…of my soul…”. If
c is an instance of
Beatnik, we’ll get back something like “Scabbidy dabbidy doo, blabbidy scabbidy doo”, accompanied by bongos.
Now, suppose we wanted to refine our criteria for choosing which version of
writePoetry to execute. For example, suppose we wanted to distinguish
GothUnfamiliarWithTheWorksOfWilliamWordsworth. In Java, we’d have to make two more classes, have each inherit from
Goth, and have them override the
writePoetry method again. That’s a lot of classes, and a lot of repeated code. Especially when we also need to distinguish
Wouldn’t it be nice if we could just add a field to the
familiarityWithTheWorksOfWilliamWordsworth, which would rate the familiarity on a scale of 0 to 100? Then have the
writePoetry methods get progressively more Wordsworth-like as the familiarity rises?
Yeah, multimethods can do that. No ifs, &&s, or switches about it.
Specifically, multimethods can use whatever criteria you want to decide which version of a method to call, provided you can write a function to test those criteria. You call the multimethod with whatever data you want, and it uses the function you defined to decide which method to dispatch to. You can dispatch on multiple fields of a record or map. You can dispatch on functions of multiple arguments. You can get as crazy as you want. You can have a multimethod which decides how to dispatch based on whether the first argument is a map whose keys all start with s or not, and the second argument is a vector of vectors of maps whose values all implement
Multimethods are much more powerful than protocols. They become even more powerful when you throw in the
isa? functions. I won’t go over those here, but you can read about them elsewhere. The Joy of Clojure covers them very thoroughly. With multimethods,
isa?, you can basically implement your own form of object-oriented programming. You can combine records and multimethods and get some amazingly powerful designs.