Chapter 9. Multimethods and Hierarchies

Runtime Polymorphism Without Classes

Clojure is not an object-oriented language in the traditional sense of classes and methods, although it is built on Java's object-oriented foundation.

Most mainstream object-oriented languages, such as Java and C++, use classes to define a tree-like hierarchy of types and to provide implementations of the methods supported by those types.

Clojure separates type hierarchies from method implementations, which greatly simplifies thorny issues such as multiple inheritance. In addition, it permits you to define multiple, independent hierarchies over the same types. This makes it possible to define IS-A relationships that more closely model the real world.

Multimethods

Clojure multimethods provide runtime polymorphic dispatch. That is, they permit you to define a function with multiple implementations. At runtime, the implementation that executes is determined based on the arguments to the function.

Most object-oriented languages have single-argument, type-based dispatch, meaning that the method to be run is determined solely by the type, or class, of the first argument. The method is called "on" that first argument. Both Java and C++ place that first argument before the method name to denote its special significance.

Clojure multimethods are more flexible. They support multiple dispatch, meaning the implementation can be determined by any and all arguments to the function. Also, the dispatch can be based on any feature of the arguments, not just type.

Multimethods are created with defmulti and implemented with defmethod.

(defmulti name dispatch-fn)
(defmethod multifn dispatch-value [args...] & body)

You call a multimethod like an ordinary function. When you call it, the dispatch function is immediately called with the same arguments that you gave to the multimethod. The value returned by the dispatch function is called the dispatch value. Clojure then searches for a method (defined with defmethod) with a matching dispatch value.

Suppose you are writing a fantasy role-playing game populated with different species of creatures: humans, elves, orcs, and so on. Each creature could be represented by a map, like the following:

(def a {:name "Arthur", :species ::human, :strength 8})
(def b {:name "Balfor", :species ::elf, :strength 7})
(def c {:name "Calis", :species ::elf, :strength 5})
(def d {:name "Drung", :species ::orc, :strength 6})

I used namespace-qualified keywords for species ( ::human instead of :human) for reasons that will be important later. (See Chapter 7 for an explanation of qualified keywords.)

Now you can define a multimethod that dispatches on the particular species of creature. For example, you can give each species a different style of movement:

(defmulti move :species)

(defmethod move ::elf [creature]
  (str (:name creature) " runs swiftly."))

(defmethod move ::human [creature]
  (str (:name creature) " walks steadily."))

(defmethod move ::orc [creature]
  (str (:name creature) " stomps heavily."))

When you call move, the appropriate method is invoked:

user=> (move a)
"Arthur walks steadily."
user=> (move b)
"Balfor runs swiftly."
user=> (move c)
"Calis runs swiftly."

What's happening here? When you call (move a), Clojure first calls the dispatch function for the move multimethod, which you have defined to be the keyword :species. Remember that a keyword, called on a map, returns the value of that key in the map. So (move a) calls (:species a), which returns ::human. Clojure then searches for a method of move with the dispatch value ::human, and invokes that method.

The same behavior could be implemented with a conditional. The advantage of the multimethod is that you can add new methods at any time. If you were to add a new species of creature, you could simply define another move method without changing any existing code.

The dispatch function doesn't have to be a simple keyword; it can be any arbitrary function. For example, you could use a dispatch function that categorizes creatures based on their strength:

(defmulti attack (fn [creature]
                   (if (> (:strength creature) 5)
                     :strong
                     :weak)))

(defmethod attack :strong [creature]
  (str (:name creature) " attacks mightily."))

(defmethod attack :weak [creature]
  (str (:name creature) " attacks feebly."))

When you call the attack multimethod, it first calls the anonymous fn, which returns either :strong or :weak. That keyword (the dispatch value) determines which attack method gets called:

user=> (attack c)
"Calis attacks feebly."
user=> (attack d)
"Drung attacks mightily."

Multiple Dispatch

As I said at the beginning of this chapter, multimethods support dispatching on multiple arguments. To do this, the dispatch function returns a vector. For example, in this game, you can define a multimethod that describes how two different creatures react when they meet. Let's say elves and orcs are enemies, but elves are friendly to one another:

(defmulti encounter (fn [x y]
                      [(:species x) (:species y)]))
(defmethod encounter [::elf ::orc] [elf orc]
  (str "Brave elf " (:name elf)
       " attacks evil orc " (:name orc)))
(defmethod encounter [::orc ::elf] [orc elf]
  (str "Evil orc " (:name orc)
       " attacks innocent elf " (:name elf)))
(defmethod encounter [::elf ::elf] [orc1 orc2]
  (str "Two elves, " (:name orc1)
       " and " (:name orc2)
       ", greet each other."))

Notice that the the method arguments do not have to have the same names as the multimethod's arguments, but the dispatch function and the methods must all accept the same number of arguments.

Now you can call the encounter multimethod on two creatures and see what happens:

user=> (encounter b c)
"Two elves, Balfor and Calis, greet each other."
user=> (encounter d b)
"Evil orc Drung attacks innocent elf Balfor"

Default Dispatch Values

Notice that you haven't defined encounter methods for all possible combinations of creatures. If you try to call encounter on an undefined combination, you get an error:

user=> (encounter a c)
java.lang.IllegalArgumentException:
No method in multimethod 'encounter'
for dispatch value: [:user/human :user/elf]

You could keep defining methods for each possible combination, but instead you can provide a default method implementation, which uses the keyword :default as the dispatch value.

(defmethod encounter :default [x y]
  (str (:name x) " and " (:name y)
       " ignore each other."))

The default method will be called when no other method matches:

user=> (encounter a c)
"Arthur and Calis ignore each other."

You can specify an alternate default dispatch value by adding the :default option to defmulti, like this:

(defmulti talk :species :default "other")
(defmethod talk ::orc [creature]
  (str (:name creature) " grunts."))
(defmethod talk "other" [creature]
  (str (:name creature) " speaks."))

Hierarchies

In most object-oriented languages, type hierarchies are implicitly defined by the inheritance relationships of classes and subclasses. Since classes also define method implementations, the relationships can get tricky rather quickly, especially in languages that permit multiple inheritance, such as C++. Java avoids that problem by disallowing multiple inheritance, but that in turn makes it harder to model some real-world problems.

In Clojure, type hierarchies are completely independent from method implementations, so they are more flexible than class-based inheritance. They can support almost any combination of relationships, including multiple inheritance and multiple roots.

Clojure defines one "global" hierarchy, which we will describe first. You can also create independent hierarchies, which will be covered at the end of this section.

(derive child parent)

derive creates an IS-A relationship between child and parent. The child and parent are referred to as tags, because they are used to identify a type or category. Tags may be either keywords or symbols, and (in the global hierarchy) must be namespace-qualified (see Chapter 7).

Continuing with your fantasy game, you can define "types" of creatures that share certain attributes. For example, you could say that humans and elves are "good" whereas orcs are "evil":

user=> (derive ::human ::good)
user=> (derive ::elf ::good)
user=> (derive ::orc ::evil)

You might further say that elves and orcs are "magical" creatures:

user=> (derive ::elf ::magical)
user=> (derive ::orc ::magical)

Just to make things interesting, let's add a special kind of human, a "hero":

user=> (derive ::hero ::human)

We have created the graph of relationships shown in Figure 9-1.

Example hierarchy with arrows pointing from children to parents

Figure 9-1. Example hierarchy with arrows pointing from children to parents

Querying Hierarchies

Once you have defined these relationships, you can query them with the isa? function:

(isa? child parent)

isa? returns true if the child is derived (directly or indirectly) from the parent. For example, the following code:

user=> (isa? ::orc ::good)
false
user=> (isa? ::hero ::good)
true
user=> (isa? ::hero ::magical)
false

isa? also returns true if the parent and child are the same (as defined by Clojure's = function):

user=> (isa? ::human ::human)
true

Hierarchies with Multimethods

When a multimethod is searching for the correct method to invoke, it uses the isa? function to compare dispatch values. This means that multimethods can dispatch not only on explicit types, but on derived types as well. Here's a multimethod that only works on "magical" creatures:

(defmulti cast-spell :species)

(defmethod cast-spell ::magical [creature]
  (str (:name creature) " casts a spell."))
(defmethod cast-spell :default [creature]
  (str "No, " (:name creature) " is not magical!"))

user=> (cast-spell c)
"Calis casts a spell."
user=> (cast-spell a)
"No, Arthur is not magical!"

When the dispatch value is a vector, the multimethod compares each vector element, from left to right, using isa?. This combines multiple-argument dispatch with hierarchies. For example, you could redefine your encounter multimethod based on "good" and "evil" creatures.

(defmulti encounter (fn [x y]
                      [(:species x) (:species y)]))

(defmethod encounter [::good ::good] [x y]
  (str (:name x) " and " (:name y) " say hello."))

(defmethod encounter [::good ::evil] [x y]
  (str (:name x) " is attacked by " (:name y)))

(defmethod encounter [::evil ::good] [x y]
  (str (:name x) " attacks " (:name y)))

(defmethod encounter :default [x y]
  (str (:name x) " and " (:name y)
       " ignore one another."))

user=> (encounter c a)
"Calis and Arthur say hello."
user=> (encounter a d)
"Arthur is attacked by Drung"

Hierarchies with Java Classes

Clojure's hierarchies can integrate with and extend Java's class hierarchy. In addition to symbols and keywords, the child argument to derive can also be a Java class. There aren't really any classes in the JDK that fit into your fantasy world, but you could make the (plausible) assertion that the Java Date class is evil:

user=> (derive java.util.Date ::evil)

The isa? function understands both hierarchies and Java class relationships:

user=> (isa? java.util.Date ::evil)
true
user=> (isa? Float Number)
true

As a result, you can define multimethods that dispatch on class, just like Java methods. This example, the invert multimethod, is defined to work on both Numbers (by negating them) and Strings (by reversing them):

(defmulti invert class)
(defmethod invert Number [x]
  (- x))
(defmethod invert String [x]
  (apply str (reverse x)))
user=> (invert 3.14)
-3.14
user=> (invert "hello")
"olleh"

More Hierarchy Queries

Three functions provide additional information about hierarchies.

(parents tag)
(ancestors tag)
(descendants tag)

All three return sets. parents returns the immediate parents of tag, ancestors returns all immediate and indirect parents. descendants returns all immediate and indirect children of tag.

parents and ancestors work on Java classes; descendants does not (this is a limitation of the Java type system).

user=> (parents ::orc)
#{:user/magical :user/evil}
user=> (descendants ::good)
#{:user/elf :user/hero :user/human}
user=> (parents ::hero)
#{:user/human}
user=> (ancestors ::hero)
#{:user/good :user/human}
user=> (parents java.util.Date)
#{java.lang.Object java.lang.Cloneable
  java.io.Serializable java.lang.Comparable
  :user/evil}

Note that the parents of java.util.Date include both the relationships defined by the Java class hierarchy and those you created with derive.

Resolving Conflicts

Since Clojure's hierarchies permit multiple inheritance, situations may arise in which there is more than one valid choice for a multimethod. Clojure does not know which one to choose, so it throws an exception.

As an example, consider a multimethod in your fantasy game that has dispatch values for both ::good and ::magical creatures:

(defmulti slay :species)

(defmethod slay ::good [creature]
  (str "Oh no!  A good creature was slain!"))

(defmethod slay ::magical [creature]
  (str "A magical creature was slain!"))

If you slay a human or an orc, you know what happens:

user=> (slay a)  ;; human
"Oh no!  A good creature was slain!"
user=> (slay d)  ;; orc
"A magical creature was slain!"

But what happens if you slay an elf?

user=> (slay b)
java.lang.IllegalArgumentException:
Multiple methods in multimethod 'slay' match
dispatch value: :user/elf -> :user/magical
and :user/good, and neither is preferred

The exception tells us that ::elf is derived from both ::magical and ::good and that there are methods for both.

To deal with this problem, you must specify the order in which dispatch values should be tried. The prefer-method function takes a multimethod and specifies that one dispatch value is preferred over another:

(prefer-method multimethod preferred-value other-value)

In this example, you would say:

user=> (prefer-method slay ::good ::magical)
user=> (slay b)
"Oh no!  A good creature was slain!"

The second solution (which isn't really a solution at all) is simply to remove one of the offending methods. The remove-method function takes a multimethod and a dispatch value then deletes the method with that dispatch value.

(remove-method multimethod dispatch-value)

In this example, it might be:

user=> (remove-method slay ::magical)
user=> (slay b)
"Oh no!  A good creature was slain!"

Type Tags

The type function is a more general version of class. First, type looks for :type metadata (see Chapter 8) on its argument, and returns that. If the object has no :type metadata, or if it does not support metadata, type returns the object's class:

user=> (type (with-meta {:name "Bob"} {:type ::person}))
:user/person
user=> (type 42)
java.lang.Integer
user=> (type {:name "Alice"})
clojure.lang.PersistentArrayMap

If you were to redefine your game creatures using :type metadata for the species:

(def a (with-meta {:name "Arthur", :strength 8}
                  {:type ::human}))
(def b (with-meta {:name "Balfor", :strength 7}
                  {:type ::elf}))

You could redefine the move multimethod to dispatch on type:

(defmulti move type)

(defmethod move ::elf [creature]
  (str (:name creature) " runs swiftly."))

(defmethod move ::human [creature]
  (str (:name creature) " walks steadily."))

This would permit the move multimethod to work with both metadata-enabled Clojure data structures and ordinary Java objects:

(defmethod move Number [n]
  (str "What?! Numbers don't move!"))

user=> (move a)
"Arthur walks steadily."
user=> (move b)
"Balfor runs swiftly."
user=> (move 6.022)
"What?! Numbers don't move!"

User-Defined Hierarchies

In addition to the global hierarchy, you can create your own independent hierarchies. The make-hierarchy function returns a new hierarchy (essentially a map of parent/child relationships). The derive, isa?, parents, ancestors, and descendants functions all accept an extra first argument that specifies the hierarchy to use.

Unlike the global hierarchy, user-defined hierarchies allow unqualified (no namespace) keywords or symbols as tags.

Be careful when creating user-defined hierarchies with derive, because its behavior is slightly different. When called with two arguments, derive modifies the global hierarchy. But user-defined hierarchies are immutable, like Clojure's other data structures, so the three-argument version of derive returns the modified hierarchy. This can be seen in the following example:

user=> (def h (make-hierarchy))
user=> (derive h :child :parent)
user=> (isa? h :child :parent)
false

Therefore, to construct a user-defined hierarchy, you must thread it through the derive statements, as in this example:

user=> (def h (-> (make-hierarchy)
                  (derive :one :base)
                  (derive :two :base)
                  (derive :three :two)))
user=> (isa? h :three :base)
true

Another alternative is to use one of Clojure's mutable reference types, such as a Var:

user=> (def h (make-hierarchy))
user=> (isa? h :child :parent)
false
user=> (alter-var-root (var h) derive :child :parent)
user=> (isa? h :child :parent)
true

By default, multimethods use the global hierarchy. The defmulti form accepts an optional argument, :hierarchy, followed by a different hierarchy to use.

Summary

Multimethods are very flexible, but that flexibility comes at a cost: they are not very efficient. Consider what happens every time you invoke a multimethod: it has to call the dispatch function, look up the dispatch value in a hash table, then perform one or more isa? comparisons to find the correct method. Even a smart compiler like Hotspot has trouble optimizing that sequence.

As a result, multimethods are probably not suitable for "low-level" functions that get called very frequently. That's why none of Clojure's built-in functions are multimethods. They are, however, an excellent tool for building extensible "high-level" APIs. Protocols, introduced in Clojure 1.2 and described in Chapter 13, offer a more restricted form of method dispatch with better performance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.211.106