Polymorphism is the ability to use multiple types as though they were the same—
that is, you can write the same code to operate on many different types. This kind of abstraction allows you to substitute different types or implementations without having to change all code that touches objects of those types. Polymorphism’s ability to reduce the surface area between different parts of a program and easily
This chapter covers
■ Polymorphism and its types
■ Clojure multimethods for ad hoc polymorphism
■ Using multi-argument dispatch
■ Querying, modifying, and creating dispatch hierarchies
99 Polymorphism and its types
substitute some parts for other parts is what makes some form of it essential in larger systems. In a certain sense polymorphism provides the ability to create your own abstractions.
There are multiple ways to achieve polymorphism, but three are common to many languages: parametric, ad hoc, and subtype polymorphism. We’ll concentrate on what these look like in Clojure without multimethods but also glance at how other lan- guages achieve the same kinds of polymorphism.
4.1.1 Parametric polymorphism
You’ve actually already come in contact with polymorphism in Clojure. As you saw in chapter 2, functions such as get, conj, assoc, map, into, reduce, and so on accept many different types in their arguments but always do the correct thing. Clojure col- lections are also polymorphic because they can hold items of any type. This kind of polymorphism is called parametric polymorphism because such code mentions only parameters and not types. It’s common in dynamically typed languages because such lan- guages by their nature don’t often mention types explicitly. But it’s also present in some statically typed programming languages, both object-oriented languages such as Java and C# (where it’s called generics) and functional programming (OO) languages such as ML and Haskell.
This kind of polymorphism is usually invisible in Clojure: you just use the built-in function and collection types, and the Clojure runtime works out what should happen or throws an exception (or often returns nil) if the type doesn’t work with that func- tion. Underneath the covers are Java classes and interfaces checking types and imple- menting the polymorphism. It’s possible in Clojure to create new types that work with Clojure built-in functions such as conj using a mix of Java interop and user-defined Clojure types: we’ll demonstrate this technique in chapter 9.
But in Clojure if you want to create your own parametrically polymorphic func- tions, you need to look to other kinds of polymorphism to implement them. This seems like a curious statement: How can you implement one type of polymorphism using another? How can a piece of code represent two kinds of polymorphism simul- taneously? Polymorphism is often a matter of perspective: from the perspective of the calling code (the code using your functions and types), your code can appear paramet- rically polymorphic—that’s often precisely the goal. But internally, hidden from the caller, your code may use a form of polymorphism that’s explicit about types. That’s where the other two forms of polymorphism come in.
4.1.2 Ad hoc polymorphism
Ad hoc polymorphism is simply enumerating each possible type a function can use and writing an implementation for each one. You can recognize this pattern in Clojure easily: some function is called on the argument to produce a dispatch value, and cond, case, or condp selects an implementation with a matching dispatch value to produce a
result. Here’s an example polymorphic function that simply returns a string naming its argument’s type:
(defn ad-hoc-type-namer [thing]
(condp = (type thing) java.lang.String "string"
clojure.lang.PersistentVector "vector"))
;=> #'user/ad-hoc-type-namer (ad-hoc-type-namer "I'm a string")
;=> "string"
(ad-hoc-type-namer [])
;=> "vector"
(ad-hoc-type-namer {}) IllegalArgumentException No matching clause: class
clojure.lang.PersistentArrayMap user/ad-hoc-type-namer (NO_SOURCE_FILE:2)
Notice that this example of ad hoc polymorphism doesn’t allow calling code to “train”
the ad-hoc-type-namer function to understand new types—you can’t add new clauses to the condp expression without rewriting the function. This property is called closed dispatch because the list of available implementations (that is, to which you can dis- patch) can’t be changed from the outside. But you can implement open dispatch by keeping the implementations outside your type-naming function:
(def type-namer-implementations {java.lang.String (fn [thing] "string") clojure.lang.PersistentVector (fn [thing] "vector")})
;=> #'user/type-namer-implementations (defn open-ad-hoc-type-namer [thing]
(let [dispatch-value (type thing)]
(if-let [implementation
(get type-namer-implementations dispatch-value)]
(implementation thing)
Ad hoc polymorphism as “function overloading”
Other languages often call ad hoc polymorphism function overloading and have some special syntax to support this type of polymorphism. For example, in Java you can repeat the method but annotate the argument’s types differently; the Java virtual machine (JVM) will then perform the dispatch to the right method invisibly at compile time. Here’s a short example equivalent to the ad-hoc-type-namer Clojure function:
public class TypeNamer extends Object { // ...
public String typeName(String thing) { return "string"; } public String typeName(PersistentVector thing) {
return "vector";
} }
Call type on thing argument to return type of thing
Dispatch for implementation of function appropriate to type; in this case, type-specific implementation is simply string "string"
If there’s a type this function doesn’t know how to handle, it throws exception.
Pull implementations out into a separate redef-able map Use a dispatch value as key to implementation map If find
implementation for dispatch value, use it and return result
101 Polymorphism and its types
(throw (IllegalArgumentException.
(str "No implementation found for " dispatch-value))))))
;=> #'user/open-ad-hoc-type-namer
(open-ad-hoc-type-namer "I'm a string")
;=> "string"
(open-ad-hoc-type-namer [])
;=> "vector"
(open-ad-hoc-type-namer {}) IllegalArgumentException No implementation found for class clojure.lang.PersistentArrayMap user/open-ad-hoc-type-namer (NO_SOURCE_FILE:5)
(def type-namer-implementations (assoc type-namer-implementations
clojure.lang.PersistentArrayMap (fn [thing] "map")))
;=> #'user/type-namer-implementations (open-ad-hoc-type-namer {})
;=> "map"
Ad hoc polymorphism is simple and easy to understand, but it’s done from the per- spective of the implementations that use a type, not from the perspective of the types being used. The next form of polymorphism is more from the type’s perspective.
4.1.3 Subtype polymorphism
So far we’ve been concentrating on the functions that are polymorphic, but the types can also be polymorphic. Subtype polymorphism is a kind of polymorphism where one type says it can be substituted for another so that any function that can use one type can safely use the other. Stated more simply, one type says it’s “a kind of” another type, and code that understands more general kinds of things will automatically work cor- rectly with more specific things of the same general kind.
Subtype polymorphism is the dominant kind of polymorphism in OO languages and it’s expressed as class or interface hierarchies. For example, if a Person class inherits from (or extends) an Animal class, then any method that works with Animal objects should automatically work with Person objects too. Some dynamic OO lan- guages also allow a form of subtype polymorphism (called structural subtyping) that doesn’t use an explicit hierarchy or inheritance. Instead methods are designed to work with any objects that have the necessary structure, such as properties or methods with the correct names. Python, Ruby, and JavaScript all allow this form of subtyping, which they call duck typing.
Clojure can use Java classes and interfaces through its Java interop features and Clojure’s built-in types participate in Java interfaces and class hierarchies. For exam- ple, remember in a footnote in chapter 2 we briefly mentioned that Clojure map lit- erals may be array-map when they’re small but become hash-map when they get larger? In Java there’s an abstract class that they share, as you can see in the follow- ing example:
(defn map-type-namer [thing]
(condp = (type thing) Otherwise
throw an exception just like before
Normal cases from before still work.
But it still doesn’t understand maps.
So redefine implementation map, adding a case for PersistentArrayMap.
open-ad-hoc-type-namer now understands maps.
First attempt with ad hoc polymorphism checks each type explicitly.
clojure.lang.PersistentArrayMap "map"
clojure.lang.PersistentHashMap "map"))
;=> #'user/map-type-namer (map-type-namer (hash-map))
;=> "map"
(map-type-namer (array-map))
;=> "map"
(map-type-namer (sorted-map)) IllegalArgumentException No matching clause: class
clojure.lang.PersistentTreeMap com.gentest.ConcreteClojureClass/map-type- namer (NO_SOURCE_FILE:2)
(defn subtyping-map-type-namer [thing]
(cond
(instance? clojure.lang.APersistentMap thing) "map"
:else (throw (IllegalArgumentException.
(str "No implementation found for ") (type thing)))))
;=> #'user/subtyping-map-type-namer (subtyping-map-type-namer (hash-map))
;=> "map"
(subtyping-map-type-namer (array-map))
;=> "map"
(subtyping-map-type-namer (sorted-map))
;=> "map"
By writing code that knows how to use APersistentMap, you know it works with any subtype using your single implementation; it even works for types that don’t exist yet as long as they’ll extend APersistentMap.
Clojure also offers some ways to create subtypes of your own using multimethod hierarchies (which we’ll look at later in this chapter) or protocols (which we’ll discuss in chapter 9), but beyond this Clojure offers no rigid notion of subtyping. The reason is that subtyping, while powerful because it allows you to write fewer implementations of functions, can also be restraining if applied too broadly because there’s often no one, single, universally applicable arrangement of types. For example, if you’re writ- ing geometry code, it may be simpler for some functions to see a circle as a special case of an ellipse but for others to view an ellipse as a special case of a circle. You may also have a single thing belong to multiple, nonoverlapping type hierarchies at the same time: a person is a kind of material body to a physicist but a kind of animal to a biologist. More importantly, Clojure is focused on data and values, not types: the programming-language type used to contain some information (such as a map, list, or vector) is independent of the problem-domain type (such as animal, vegetable, or min- eral), and it’s the latter that should be the focus. But this means your own code, and not your programming language, will need to distinguish somehow between maps that represent animals and maps that represent minerals or to know that an animal can be represented with either a list form or a map form.
Notice duplicate implementation
There was a case missed.
Second attempt using subtype polymorphism
Use instance? to query Java’s class hierarchy; APersistent-Map is Java superclass of all maplike things in Clojure
Function now works for anything maplike.
103 Polymorphism using multimethods
Multimethods provide features to express both ad hoc and subtype polymorphism and even to express multiple different kinds of subtype polymorphism at the same time. Let’s leave polymorphic theory behind us now and look more concretely at how multimethods can help you write polymorphic code.