Chapter 4. Polymorphism with multimethods

 

This chapter covers

  • What polymorphism means
  • What method dispatch is
  • Clojure multimethods and how they work

 

Many years ago, when I was learning the Java programming language, I read a book by Bruce Eckel called Thinking in Java. Although I thought it was a great book and learned a lot from it, over the years I’ve realized that the title of the book is a specific instance of a common curse. It’s a curse that afflicts a majority of program-mers—if programmers know only a specific language, they soon begin to think only in terms of what’s possible (expressible) in that language.

In our industry, this is manifested in several ways. The most common is that, on average, a programmer fluent in several languages is a better programmer than a programmer who knows only one language. This correlation is because each language has slightly (sometimes very) different concepts and models that programs are expressed in (for instance, objects in Java/C++, prototypes in JavaScript, functions in Haskell, and so on). The corollary to this observation is that a programmer familiar with many different languages is often an even better programmer than the programmer who’s familiar with several similar ones.

There’s a point to this introduction, and it’s that popular OO languages such as Java and C++ mold the general notion of what is and what isn’t object-oriented. Here is what Alan Kay said about this: “Actually, I invented the term ‘object-oriented’, and I can tell you I did not have C++ in mind.”

As mentioned in chapter 1, the truth is that the idea of objects is far more nuanced than the common notions of it, and by the end of this book, you’ll hopefully have a rather different opinion of objects than the one you might have today. We’ll start off this chapter by exploring a pillar of OO—polymorphism—and see how Clojure deals with it.

4.1. Polymorphism

The word polymorphism derives from ancient Greek, where poly means many and morph means form. In programming languages, polymorphism is the idea that values of different data types can be treated in the same way. Often, this is manifested via class inheritance, where classes in a hierarchy can have methods of the same name. When such a method is called on an object, the right code is executed at runtime depending on which class the object is an instance of. There are various ways of achieving polymorphic behavior, and inheritance is only one such way. The diagram in figure 4.1, taken from a paper published by Luca Cardelli in 1985 (http://lucacardelli.name/Papers/OnUnderstanding.pdf), shows a high-level classification of polymorphism.

Figure 4.1. There are several types of polymorphism. Subtype polymorphism (a form of inclusion) is the most common (Java/C++) kind of polymorphism but is only one of several.

We won’t get into the details of all these types of polymorphism (the previously mentioned paper is highly recommended). Instead, we’ll concentrate on subtype polymorphism (which is a form of inclusion polymorphism), because it’s the most commonly implemented form of polymorphism. Examples of languages that offer this type include Java, C++, and Ruby. The following sections offer a primer that paves the way to understanding how Clojure approaches polymorphism. Specifically, we’ll explore basic subtype polymorphism, followed by duck typing. Then we’ll look at method dispatch, starting with the commonly available single dispatch, followed by double and multiple dispatches. Then, you’ll be ready to deal with Clojure.

4.1.1. Subtype polymorphism

Subtype polymorphism, as illustrated in figure 4.2, is a form of inclusion polymorphism that establishes the basis of polymorphic behavior through an inheritance relationship. Languages such as Java express such a relationship using the extends keyword. This is the famous dynamic polymorphism that’s common in literature about Java and C++. In a situation where there’s an inheritance hierarchy, and a particular method has been overridden across parts of it, which method is executed depends on the type of the receiver (the object on which a method is called). It’s dynamic because this determination of the type of the receiver happens at runtime.

Figure 4.2. An example of the commonly found subtype polymorphism. Which makeSound gets called depends on the type of receiver and is determined at runtime.

To be as clear as possible, let’s imagine a function recordSound that accepted an argument named anAnimal of the Animal type. When the makeSound method is called on the anAnimal object, the correct method is called, thanks to subtype polymorphism. Although this form of polymorphism is present in several programming languages, Clojure doesn’t support it. (To be specific, it doesn’t support it out of the box. We’ll create our own little subtype-polymorphic DSL later.)

As we said earlier, this isn’t the only way languages implement polymorphism. Languages such as Ruby have another form of polymorphism called duck typing.

4.1.2. Duck typing

Duck typing describes a form of dynamic polymorphism similar to the one we talked about previously but without inheritance. James Whitcomb Riley (who has nothing to do with computer science) coined the term when he noted, “When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck” (http://en.wikipedia.org/wiki/Duck_test).

In a weakly typed language, the recordSound function from the previous example could work on any type of object as long as it implemented a makeSound method. There need be no special relationship between objects that can be accepted as arguments here; all they have to do is respond appropriately to the makeSound method call. Languages such as Ruby and Python support duck typing.

The disadvantage is that duck typing alone is insufficient to implement more than one method of the same name that accepts different types of arguments. To do that, subtype polymorphism is needed once again. Clojure isn’t duck typed either, because Clojure doesn’t have the traditional concept of objects.

Now that you’ve seen two commonly found variants of polymorphism, let’s examine the underlying mechanism that allows polymorphism to work—the concept of method dispatch.

4.2. Method dispatch

The key to all this dynamic behavior—which method is executed when a polymorphic method is called—is the mechanism of dynamic method dispatch. It’s a fancy way of saying that when a method is called, the name of the method is mapped to a concrete implementation at runtime.

4.2.1. Single and double dispatch

Languages such as Java support only single dispatch. What this means is that at runtime, the type of receiver is the only thing that determines which method is executed. To demonstrate this, we’ll use another commonly cited but well-suited example of a program that needs to process an abstract syntax tree (AST). Let’s imagine our AST is implemented in a Java-like OO language and is modeled via the classes shown in figure 4.3.

Figure 4.3. A simplistic hierarchy representing an AST. Each node is a subclass of a generic SyntaxNode and has functions for various tasks that a compiler or IDE might perform.

When an IDE needs to format the source code being edited, it calls format on the AST representation of the code text. Internally, the AST delegates the format call to each component. This walks the tree and ultimately calls format on each node in the AST.

This is straightforward enough. Even though the AST is made up of different kinds of concrete nodes, the operation format is defined in the generic base class and is called using a reference to that base class. Dynamically, the receiver type is determined, and the appropriate method is executed. This is called single-dispatch because only the type of receiver is used to determine which code to run. If any of these methods were to accept arguments, the types of those wouldn’t determine the method that needs to execute.

Single dispatch can lead to unexpected situations. We’ll explore one such issue with the following example. Consider a Java interface for a Person, in the domain of pet lovers:

public interface Person {
    public void pet(Dog d);
    public void pet(Cat c);
}

Let’s now define a couple of implementations of this interface. First, we’ll define Man, which might look as follows:

public class Man implements Person {
  public void pet(Dog d) {
    System.out.println("Man here, petting dog:" + d.makeSound());
  }

  public void pet(Cat c) {
    System.out.println("Man here, petting cat:" + c.makeSound());
  }
}

Let’s also add another implementation of the same interface, in the form of the Woman class:

public class Woman implements Person{
  public void pet(Dog d) {
    System.out.println("Woman here, petting dog:" + d.makeSound());
  }

  public void pet(Cat c) {
       System.out.println("Woman here, petting cat:" + c.makeSound());
  }
}

The purpose of the code so far is to construct a class hierarchy that we can then use to deal with a different one: that of the Animals. We’ll start with the interface:

public interface Animal {
    public String makeSound();
}

We’ll create the two implementations we referred to in our Person interface, the Dog and the Cat:

public class Dog implements Animal{
     public String makeSound() {
         return "Bow, wow";
     }
}

public class Cat implements Animal {
     public String makeSound() {
         return "Meow";
     }
}

Finally, let’s imagine we want to use all these classes together. We’ll create a Park class where we might have instances of our Person interface hanging out with instances of our Animal interface. It might look like this:

public class Park {
  public static void main(String[] args) {
     Person p = new Man();
     Animal a = new Dog();
     p.pet(a);
  }
}

Unfortunately, this Java code won’t even compile. The compiler will say that there’s no such method pet that accepts an instance of the Animal class. What we’d like (and what we intended) is for Java to accept this and then at runtime discern that p is of type Man and a is of type Dog. We’d then like for it to call the right method inside the Man class.

But it can’t. Java can only look at the type of the receiver (the object p). In this case, it’s declared as Person and is determined to be Man at runtime. In order to find out what method to call, Java would need to know the runtime type of a (which in this case is Dog). But it doesn’t do that. Because a is declared to be of type Animal, it looks for a method inside Man with the signature pet(Animal) and doesn’t find it. This results in the compiler error.

This is the whole single-dispatch problem, and it occurs only when you have more than one hierarchy (as we do: Animal and Person). The visitor pattern is an attempt to work around it, and it does so by using a callback. We’ll examine this in the next section.

4.2.2. The visitor pattern (and simulating double dispatch)

Our Park class doesn’t work because only the type of the receiver object is used to determine which method to call. The type of the Animal object is ignored in a single-dispatch scenario. In a language that resolved methods through double dispatch, the types of the receiver and the first argument would be used to find the method to execute. This would then work as desired.

We can simulate double dispatch in Java and other languages that suffer from single dispatch. That’s exactly what the visitor pattern does. We’ll examine that here, and later you’ll see why it’s not needed in Clojure.

Consider the program we mentioned earlier that needed to process an AST. The implementation from there implemented things like checkValidity and generate-ASM inside each concrete implementation of SyntaxNode. The problem with this approach is that adding new operations like this is quite invasive, because every type of node needs to be touched. The visitor pattern allows such operations to be separated from the data object hierarchy (which, in this case, is the SyntaxNode hierarchy). Let’s take a quick look at the modification needed, as shown in figure 4.4, followed by the new visitor classes. Figure 4.5 shows the new visitor classes.

Figure 4.4. A modification is needed to simulate double dispatch. The accept method is a somewhat unclear but required method in each node, which will call back the visitor.

Figure 4.5. Each visitor class does one operation and knows how to process each kind of node. Adding new kinds of operation involves adding new visitors.

The infrastructure method accept(NodeVisitor) in each SyntaxNode is required because of the need to simulate a double dispatch. Inside it, the visitor itself is called back using an appropriate visit method. Here’s some more Java code showing an example involving AssignmentNode:

public void accept(NodeVisitor visitor) {
  visitor.visitAssignment(this);
}

This is a lot of indirection for something that ought to be simple. Unfortunately, the limitation of single dispatch requires this forced simulation of double dispatch. Again, if the language natively supported double dispatch, all this boilerplate code would be unnecessary. Further, the NodeVisitor hierarchy needs to know about all the classes in the SyntaxNode hierarchy, which makes it a more coupled design.

Still, now that you know about double dispatch, the obvious question is, what about triple dispatch? What about dispatching on any number of argument types?

4.2.3. Multiple dispatch

In essence, multiple dispatch takes the idea of double dispatch to its logical end. A language that supports multiple dispatch can look up methods based on the type of the receiver and all the arguments passed to it.

A language that supports this feature doesn’t need the convoluted visitor pattern. Simple and straightforward calls with multiple arguments of different types work fine. Figure 4.6 shows how this arrangement might look.

Figure 4.6. With multiple dispatch, visitors can have a straightforward, polymorphic visit method (off the type of SyntaxNodes), and SyntaxNode doesn’t need the accept method infrastructure.

Languages such as Dylan, Nice, R, and Common Lisp support multiple dispatch. Clojure also supports multiple dispatch through its multimethods feature.

4.3. Multimethods

Now that you’ve seen how method polymorphism works underneath, we’re finally ready to look at Clojure’s multimethod feature. Clojure multimethods support not only multiple dispatch but much more. Indeed, once you look past multiple dispatch, a commonly asked question is whether a language can dispatch on things other than the types of values. With Clojure’s multimethods, methods can be made to dispatch based on any arbitrary rule.

A multimethod is defined using defmulti. A multimethod by itself isn’t useful; it needs candidate implementations from which to choose when it’s called. The defmethod macro is used to define implementations. Let’s take a look.

4.3.1. Without multimethods

Consider the situation where our expense-tracking service has become popular. We’ve started an affiliate program where we pay referrers if they get users to sign up for our service. Different affiliates have different fees. Let’s begin with the case where we have two main ones: mint.com and the universal google.com.

We’d like to create a function that calculates the fee we pay to the affiliate. For the sake of illustration, let’s decide we’ll pay our affiliates a percentage of the annual salary the user makes. We’ll pay Google 0.01%, we’ll pay Mint 0.03%, and everyone else gets 0.02%. Let’s write this without multimethods first:

(defn fee-amount [percentage user]
  (float (* 0.01 percentage (:salary user))))

(defn affiliate-fee-cond [user]
  (cond
    (= :google.com (:referrer user)) (fee-amount 0.01 user)
    (= :mint.com (:referrer user)) (fee-amount 0.03 user)
    :default (fee-amount 0.02 user)))

The trouble with this way of writing this function is that it’s painful to add new rules about affiliates and percentages. The cond form will soon get messy. We’ll now rewrite it using Clojure’s multimethods.

4.3.2. Using multimethods

Before we implement the same functionality using multimethods, let’s take a moment to understand how they work. As mentioned earlier, multimethods are declared using the defmulti macro. Here’s a simplified general form of this macro:

(defmulti name dispatch-fn & options)

The dispatch-fn function is a regular Clojure function that accepts the same arguments that are passed in when the multimethod is called. The return value of dispatch-fn is what is called a dispatching value. Before moving on, let’s look at the previously mentioned defmethod macro:

(defmethod multifn dispatch-value & fn-tail)

This creates a concrete implementation for a previously defined multimethod. The multifn identifier should match the name in the previous call to defmulti. The dispatch-value will be compared with the return value of the dispatch-fn from earlier in order to determine which method will execute. This will be clearer through an example, so let’s rewrite the affiliate-fee function using multimethods:

(defmulti affiliate-fee :referrer)

(defmethod affiliate-fee :mint.com [user]
  (fee-amount 0.03 user))

(defmethod affiliate-fee :google.com [user]
  (fee-amount 0.01 user))

(defmethod affiliate-fee :default [user]
  (fee-amount 0.02 user))

That looks a lot cleaner than the cond form, because it separates each case into its own method (which looks somewhat similar to a plain function). Let’s set up some test data, so you can try it out:

(def user-1 {:login "rob" :referrer :mint.com :salary 100000})
(def user-2 {:login "kyle" :referrer :google.com :salary 90000})
(def user-3 {:login "celeste" :referrer :yahoo.com :salary 70000})

And now try a few calls to affiliate-fee:

(affiliate-fee user-1)
30.0
(affiliate-fee user-2)
9.0
   (affiliate-fee user-3)
14.0

This works as expected. When a call is made to affiliate-fee, the multimethod first calls the dispatch-fn. In this case, we used :referrer as the dispatch-fn. The arguments to the dispatch-fn are the same as the arguments to the multimethod, which in this case is the user hash map. Calling :referrer on the user object returns something like :google.com and is considered the dispatch-value. The various methods are then checked over, and when a match is found, it’s executed.

If a match isn’t found, then the default case is picked. The default value for this catchall case is :default, but you can specify anything you want when calling defmulti, as so:

(defmulti affiliate-fee :referrer :default :else)

After you change the default value with such a call, the default case method would need to use the same value:

(defmethod affiliate-fee :else [user]
  (fee-amount 0.02 user))

It’s that simple! Now, to add new cases, you add new methods, which is far cleaner than ending up with a long-winded cond form. Now let’s try to stretch this example a bit by expanding the capabilities of our system.

4.3.3. Multiple dispatch

Imagine that our service is even more successful than before and that the affiliate program is working great. So great, in fact, that we’d like to pay more profitable users a higher fee. This would be a win-win situation for the affiliate network and our service, so let’s get started by quantifying a user’s level of profitability. Consider the following dummy implementation of this functionality:

(defn profit-rating [user]
  (let [ratings [::bronze ::silver ::gold ::platinum]]
    (nth ratings (rand-int (count ratings)))))

This is quite the dummy implementation, as you can see; it doesn’t even use the user parameter. It serves our purpose nicely, because it demonstrates that this function could be doing anything (number crunching, database access, web-service calls, whatever you like). In this case, it returns one of the four possible ratings out of ::bronze, ::silver, ::gold, or ::platinum. The extra colon resolves each of these keywords to the namespace it’s used in. As you’ll see shortly, creating ad hoc hierarchies in Clojure requires the use of namespace-qualified keywords if they’re used as dispatch values.

Now let’s consider the business rules shown in table 4.1.

Table 4.1. Affiliate fee business rules

Affiliate

Profit rating

Fee (% of salary)

mint.com Bronze 0.03
mint.com Silver 0.04
mint.com Gold/platinum 0.05
google.com Gold/platinum 0.03

From the rules it’s clear that there are two values based on which fee percentage is calculated: the referrer and the profit rating. In a sense, the combination of these two values is the affiliate fee type. Because we’d like to dispatch on this virtual type, comprising two values, let’s create a function that computes the pair:

(defn fee-category [user]
  [(:referrer user) (profit-rating user)])

This returns a vector containing two values, for instance, [:mint.com ::bronze], [:google.com ::gold], and [:mint.com ::platinum]. We can use these as dispatch values in our methods, as follows:

(defmulti profit-based-affiliate-fee fee-category)
(defmethod profit-based-affiliate-fee [:mint.com ::bronze] [user]
  (fee-amount 0.03 user))
(defmethod profit-based-affiliate-fee [:mint.com ::silver] [user]
  (fee-amount 0.04 user))
(defmethod profit-based-affiliate-fee [:mint.com ::gold] [user]
  (fee-amount 0.05 user))
(defmethod profit-based-affiliate-fee [:mint.com ::platinum] [user]
  (fee-amount 0.05 user))
(defmethod profit-based-affiliate-fee [:google.com ::gold] [user]
  (fee-amount 0.03 user))
(defmethod profit-based-affiliate-fee [:google.com ::platinum] [user]
  (fee-amount 0.03 user))
(defmethod profit-based-affiliate-fee :default [user]
  (fee-amount 0.02 user))

This reads a lot like our table with business rules, and adding new rules is still quite easy and doesn’t involve modifying existing code.

On a separate note, this is a form of double dispatch because we’re dispatching on two values. The affiliate-id and profit-level, even though they aren’t types in the class-based sense of the word, behave as types in this domain. This Clojure code performs a double dispatch based on types in our domain. There’s nothing to stop you from dispatching on any number of such types if you wanted to; the mechanism is straightforward.

 

Simulating single dispatch with multimethods

Incidentally, it’s trivial to simulate single dispatch in Clojure—all that’s needed is a dispatch function that ignores all but the first argument. If the first argument is a data type (or a class), you can even simulate Java-style single-dispatch.

 

Although we’ve shown polymorphic behavior in these examples, we’ve not mentioned inheritance at all. This shows that inheritance isn’t a necessary condition for polymorphism. But inheritance can be quite convenient, as you’ll see in the next section.

4.3.4. Ad hoc hierarchies

Now that we have the profit-rating infrastructure in place, we might want to expose the ratings to our members in a form similar to airlines’ membership status. We could treat bronze as an entry-level status, and members could graduate to silver and higher levels. We could also treat gold and platinum as premier statuses, affording such members a higher class of service. This classification might look like that shown in figure 4.7.

Figure 4.7. Our domain demands a hierarchy of membership statuses. We can use this external-facing classification to simplify our code by defining an ad hoc hierarchy reflecting this structure.

We can codify this hierarchy using Clojure’s derive function, as follows:

(derive ::bronze ::basic)
(derive ::silver ::basic)
(derive ::gold ::premier)
(derive ::platinum ::premier)

This is why we namespace-qualified the keywords; we can now use them in hierarchies as shown here. Having done so, we can programmatically ensure it works as expected using the isa? function:

(isa? ::platinum ::premier)
true

(isa? ::bronze ::premier)
false

Because we now have a hierarchy, we can redefine the multimethod to calculate the affiliate fee as follows:

(defmulti affiliate-fee-for-hierarchy fee-category)
(defmethod affiliate-fee-for-hierarchy [:mint.com ::bronze] [user]
  (fee-amount 0.03 user))
(defmethod affiliate-fee-for-hierarchy [:mint.com ::silver] [user]
  (fee-amount 0.04 user))
(defmethod affiliate-fee-for-hierarchy [:mint.com ::premier] [user]
  (fee-amount 0.05 user))
(defmethod affiliate-fee-for-hierarchy [:google.com ::premier] [user]
  (fee-amount 0.03 user))
(defmethod affiliate-fee-for-hierarchy :default [user]
  (fee-amount 0.02 user))

This is even more succinct; it also captures the intent that acquiring a premier member from an affiliate partner is more expensive.

Java class hierarchy

Although we’ve shown that it’s easy enough to create ad hoc hierarchies, Clojure goes one step further to make things simpler by providing support for Java classes out of the box. When Java classes are used as dispatch values, inheritance relationships are automatically respected, relieving the need for the hierarchy to be created again via calls to derive.

This ensures that both javax.swing.JComboBox and javax.swing.JFileChooser automatically treat javax.swing.JComponent as a parent. A method that uses the JComponent as a dispatch value will match if the dispatching function returns either a JFileChooser or a JComboBox. The programmer doesn’t have to do anything special for this to work.

The visitor pattern revisited

Now that you know how multimethods solve the dispatch problem, let’s rewrite the AST program using multimethods. Imagine we represent a couple of syntax nodes as so:

(def aNode {:type :assignment :expr "assignment"})
(def vNode {:type :variable-ref :expr "variableref"})

We’ll use the appropriately named :type value of the hash map to behave as the type on which we’ll dispatch our multimethod. Here’s the implementation of checkValidity:

(defmulti checkValidity :type)
(defmethod checkValidity :assignment [node]
  (println "checking :assignment, expression is" (:expr node)))
(defmethod checkValidity :variable-ref [node]
  (println "checking :variable-ref, expression is" (:expr node)))

Similarly, here’s the multimethod for generateASM:

(defmulti checkValidity :type)
(defmethod checkValidity :assignment [node]
  (println "checking :assignment, expression is" (:expr node)))
(defmethod checkValidity :variable-ref [node]
  (println "checking :variable-ref, expression is" (:expr node)))

This is much simpler than creating the whole double-dispatch mechanism in a language like Java or C++ that doesn’t support it. In order to add new types of operations on the AST, you create a new multimethod.

We’ve covered creation and usage of multimethods. Before moving on, let’s look at the situation where more than one dispatching value matches, which confuses the multimethod.

Resolving conflicts

Sometimes, a hierarchy may involve what looks like multiple inheritance. Consider the situation where a programmer can be both an employee and a geek, as shown in figure 4.8.

Figure 4.8. A hierarchy could lead to multiple inheritance. Clojure doesn’t do anything to prevent this, but it provides an elegant way to break a tie if multiple dispatch values match.

To reflect this, you’d call derive with the following:

(derive ::programmer ::employee)
(derive ::programmer ::geek)

Such multiple-inheritance relationships are perfectly valid in several domains, and Clojure doesn’t stop the programmer from creating or using them. But when methods are created with dispatch values of ::employee and ::geek, and the dispatch function returns ::programmer, the multimethod doesn’t know which one to pick because they’re both valid.

This multiple-inheritance problem is by no means unique to Clojure. Languages like Java avoid it by disallowing classes from being derived from more than one parent class. In Clojure, you can break the tie by specifying your order of preference using the prefer-method function. Here’s how you’d specify that being a geek trumps being employed:

(prefer-method multimethod-name ::geek ::employee)

Read this as a preference of ::geek over ::employee.

We’ve covered the mechanics of multimethods and shown how they’re a superior way of achieving polymorphism to what languages limited to single dispatch can ever provide. Having said that, multimethods aren’t used much, mostly because the functional programming style makes this kind of polymorphism less needed. When there’s a need, they can be an elegant solution. In the next section, we’ll examine an example of where multimethods are used to great effect.

4.3.5. Redis-clojure

Redis is a key-value database. It is fast and persistent. Redis-clojure is a Clojure client for the Redis server written by Ragnar Dahlen. The library is open source and is hosted on github.com. It uses a multimethod to parse responses sent by the server as the client communicates with it.

When the server sends a response, it prefaces the data being asked for with a single character. These control characters are shown in table 4.2. Ragnar has created a multi-method called parse-reply, which processes the responses from the server.

(defmulti parse-reply reply-type :default :unknown)

From what we showed earlier, the default dispatch value has been set to :unknown.

Table 4.2. Control characters sent by the Redis server

Character

Meaning

An error
+ Single-line reply
$ Bulk data
* Multi-bulk data
: Integer number

There are six implementations (methods) of this multimethod—one for each of the five control characters and one for the default case. The following listing shows relevant parts of the code (only the multimethod portion is listed). The choice of a multimethod here keeps the code in the next listing clean, easy to read, and easy to modify.

Listing 4.1. Implementing the Redis protocol using multimethods
(defmulti parse-reply reply-type :default :unknown)

(defmethod parse-reply :unknown
  [#^BufferedReader reader]
  (throw (Exception. (str "Unknown reply type:"))))

(defmethod parse-reply -
  [#^BufferedReader reader]
  (let [error (read-line-crlf reader)]
    (throw (Exception. (str "Server error: " error)))))

(defmethod parse-reply +
  [#^BufferedReader reader]
  (read-line-crlf reader))

(defmethod parse-reply $
  [#^BufferedReader reader]
  (let [line (read-line-crlf reader)
        length (parse-int line)]
    (if (< length 0)
      nil
      (let [#^chars cbuf (char-array length)]
        (do
          (do-read reader cbuf 0 length)
          (read-crlf reader) ;; CRLF
          (String. cbuf))))))

(defmethod parse-reply *
  [#^BufferedReader reader]
  (let [line (read-line-crlf reader)
        count (parse-int line)]
    (if (< count 0)
       nil
       (loop [i count
             replies []]
        (if (zero? i)
          replies
          (recur (dec i) (conj replies (read-reply reader))))))))

(defmethod parse-reply :
  [#^BufferedReader reader]
  (let [line (trim (read-line-crlf reader))
        int (parse-int line)]
     int))

You’ve seen the basics of multimethods. We’re now ready to move on; the next chapter shows how easy Clojure makes interoperability with its host language, Java.

4.4. Summary

As you’ve seen, what’s generally referred to as polymorphism in the context of languages such as Java and C++ is a rather specific variety called subtype polymorphism. Further, the implementation in such languages is limited to single dispatch, which is again a specific case of the far more general possibility of multiple dispatch.

These languages subscribe to a view of OOP that demands methods (and data) belong to the enclosing class. It’s probable that this is the reason these languages support only single dispatch—they dispatch only on the receiver because it’s the class that owns the potentially polymorphic method.

In Clojure, a multimethod doesn’t belong to anything. The concrete methods instead belong to the multimethod, and they can be dispatched based off any number of types. In fact, the dispatch function is an ordinary function written by the programmer, and it can do anything the programmer wants. It’s by no means limited to only the type of the arguments, opening up possibilities that can’t even be dreamed of in other languages. After all, if you think only in a singular dispatched language, you can’t imagine a world of multimethods.

This chapter covered an interesting feature of Clojure, and using it in the right situation will make your programs richer. The next chapter will focus on another great innovation of Clojure: seamless interoperability with Java code.

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

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