March 31, 2020
Hot Topics:

Clojure's Approach to Polymorphism: Method Dispatch

  • By Amit Rathore
  • Send Email »
  • More Articles »

Multiple Dispatch


A language that supports this feature doesn't need the convoluted visitor pattern. Simple, straight forward calls with multiple arguments of different types just work. Figure 4 shows how it might look.

Figure 3 shows the new visitor classes.

Polymorphism in Clojure: Method Dispatch
Figure 4. With multiple dispatch, visitors can have a straightforward, polymorphic visit method (off the type of SyntaxNodes), and SyntaxNode does not need the accept method infrastructure.



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


An Example

Consider the situation where our expense tracking service has become very 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.

Using Plain Functions

We would 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]

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


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 is a regular Clojure function that accepts the same arguments that are passed when the multimethod is called. The return value of this 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)


(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, since it separates each case into its own method (which looks somewhat similar to a plain function). Let's set up some test data, so we 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 to try a few calls to affiliate-fee:

(affiliate-fee user-1)
(affiliate-fee user-2)
   (affiliate-fee user-3)


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


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

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

Multiple Dispatch

Now let's imagine that our service is even more successful than before, and that the affiliate program is working out great. So great, in fact, that we'd like to pay them a higher fee for more profitable users. This would be a win-win 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]
       randomizer (java.util.Random. )
   (nth ratings random-index)))


Now let's consider the business rules shown in the Table 1.

Table 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 is clear that there are two values based on which the fee percentage is calculated -- the referrer and the profit rating. In a sense, the combination of these two values is the affiliate fee type. Since we'd like to dispatch on this virtual type consisting of two values, let's create a function that computes the pair:

(defn fee-category [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, like so:

(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))
  (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 since 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. In other words, the Clojure code we just wrote performs a double dispatch based on types in our domain. There's nothing to stop us from dispatching on any number of such types if we wanted to, and the mechanism is just as straightforward.

Note also that although we've seen polymorphic behavior in the examples above, we've not mentioned inheritance at all. This shows that inheritance is not a necessary condition for polymorphism. However, inheritance can be quite convenient, as we will see in the next few paragraphs.

Page 2 of 3

This article was originally published on April 26, 2010

Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.
