December 22, 2014
Hot Topics:

Clojure's Approach to Polymorphism: Method Dispatch

  • April 26, 2010
  • By Amit Rathore
  • Send Email »
  • More Articles »

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 receiver and all the arguments passed to it.

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.

Languages such as Dylan, Nice, and R support multiple dispatch. Common Lisp supports multiple dispatch via Common Lisp Object System (CLOS), which is a comprehensive DSL written on top of the base language. Clojure also supports multiple dispatch through its multimethods feature.

Multimethods

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.

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

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]
  (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, of course, is that it is painful to add new rules about affiliates and percentages. The cond form will soon get messy. We will now rewrite it using Clojure's multimethods.

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)


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 is a value will be compared with the earlier return value of the dispatch-fn in order to determine which method will execute. This will be clearer through an example, so let's now 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, 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)
30.0
(affiliate-fee user-2)
9.0
   (affiliate-fee user-3)
14.0


So this works as expected. What happens is 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 is executed. If a match is not found, then the default case is picked. The default value for this catch-all case is :default, but you can specify anything you want when calling defmulti, like so:

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


After changing 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, 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]
 (let [ratings [::bronze ::silver ::gold ::platinum]
       randomizer (java.util.Random. )
       random-index (.nextInt randomizer (count ratings))]
   (nth ratings random-index)))


This is quite the dummy implementation, as you can see; it doesn't even use the user parameter. It serves our purpose nicely though, since it demonstrates that this function could be doing anything (including things like number crunching, database access, web service calls, whatever you like). In this case, it returns one of the four possible ratings ::bronze, ::silver, ::gold, or ::platinum.

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]
  [(: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, like so:

(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 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



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

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

Sitemap | Contact Us

Rocket Fuel