Multimethods let you create ad hoc taxonomies, which can be helpful when you discover type relationships that are not explicitly declared as such.
For example, consider a financial application that deals with checking and savings accounts. Define a Clojure map for an account, using a tag to distinguish the two:
| (ns examples.multimethods.account) |
Now, you’re going to create two different checking accounts, tagged as ::checking and ::savings. The doubled :: causes the keywords to resolve in the current namespace. To see the namespace resolution happen, compare entering :checking and ::checking at the REPL:
| :checking |
| -> :checking |
| |
| ::checking |
| -> :user/checking |
Placing keywords in a namespace helps prevent name collisions with other people’s code. When you want to use ::savings or ::checking from another namespace, you’ll need to fully qualify them:
| {:id 1, :tag :examples.multimethods.account/savings, :balance 100M} |
Full names get tedious quickly, so you can use alias to specify a shorter alias for a long namespace name:
| (alias short-name-symbol namespace-symbol) |
Use alias to create the short name acc:
| (alias 'acc 'examples.multimethods.account) |
| -> nil |
Now that the acc alias is available, create two top-level test objects, a savings account and a checking account:
| (def test-savings {:id 1, :tag ::acc/savings, ::balance 100M}) |
| -> #'user/test-savings |
| |
| (def test-checking {:id 2, :tag ::acc/checking, ::balance 250M}) |
| -> #'user/test-checking |
Note that the trailing M creates a BigDecimal literal and does not mean you have millions of dollars.
The interest rate is 0% for checking accounts and 5% for savings accounts. Create a multimethod interest-rate that dispatches based on :tag, like so:
| (defmulti interest-rate :tag) |
| (defmethod interest-rate ::acc/checking [_] 0M) |
| (defmethod interest-rate ::acc/savings [_] 0.05M) |
Check your test-savings and test-checking to make sure that interest-rate works as expected.
| (interest-rate test-savings) |
| -> 0.05M |
| |
| (interest-rate test-checking) |
| -> 0M |
Accounts have an annual service charge, with rules as follows:
In a realistic example, the rules would be more complex. Premium status would be driven by average balance over time, and there would probably be other ways to qualify. But the previous rules are complex enough to demonstrate the point.
You could implement service-charge with a bunch of conditional logic, but premium feels like a type, although there’s no explicit premium tag on an account. Create an account-level multimethod that returns ::premium or ::basic:
| (defmulti account-level :tag) |
| (defmethod account-level ::acc/checking [acct] |
| (if (>= (:balance acct) 5000) ::acc/premium ::acc/basic)) |
| (defmethod account-level ::acc/savings [acct] |
| (if (>= (:balance acct) 1000) ::acc/premium ::acc/basic)) |
Test account-level to make sure that checking and savings accounts require different balance levels to reach ::premium status:
| (account-level {:id 1, :tag ::acc/savings, :balance 2000M}) |
| -> :examples.multimethods.account/premium |
| (account-level {:id 1, :tag ::acc/checking, :balance 2000M}) |
| -> :examples.multimethods.account/basic |
Now you might be tempted to implement service-charge using account-level as a dispatch function:
| ; bad approach |
| (defmulti service-charge account-level) |
| (defmethod service-charge ::basic [acct] |
| (if (= (:tag acct) ::checking) 25 10)) |
| (defmethod service-charge ::premium [_] 0) |
The conditional logic in service-charge for ::basic is exactly the kind of type-driven conditional that multimethods should help us avoid. The problem here is that you’re already dispatching by account-level, and now you need to be dispatching by :tag as well. No problem—you can dispatch on both. Write a service-charge whose dispatch function calls both account-level and :tag, returning the results in a vector:
| (defmulti service-charge (fn [acct] [(account-level acct) (:tag acct)])) |
| (defmethod service-charge [::acc/basic ::acc/checking] [_] 25) |
| (defmethod service-charge [::acc/basic ::acc/savings] [_] 10) |
| (defmethod service-charge [::acc/premium ::acc/checking] [_] 0) |
| (defmethod service-charge [::acc/premium ::acc/savings] [_] 0) |
This version of service-charge dispatches against two different taxonomies: the :tag intrinsic to an account and the externally defined account-level. Try a few accounts to verify that service-charge works as expected:
| (service-charge {:tag ::acc/checking :balance 1000}) |
| -> 25 |
| |
| (service-charge {:tag ::acc/savings :balance 1000}) |
| -> 0 |
There’s one further improvement you can make to service-charge. Since all premium accounts have the same service charge, it seems redundant to have to define two separate service-charge methods for ::savings and ::checking accounts. It would be nice to have a parent type ::account so you could define a multimethod that matches ::premium for any kind of ::account. Clojure lets you define arbitrary parent-child relationships with derive:
| (derive child parent) |
Using derive, you can specify that both ::savings and ::checking are kinds of ::account:
| (derive ::acc/savings ::acc/account) |
| (derive ::acc/checking ::acc/account) |
When you start to use derive, isa? comes into its own. In addition to understanding Java inheritance, isa? knows all about derived relationships:
| (isa? ::acc/savings ::acc/account) |
| -> true |
Now that Clojure knows that savings and checking are accounts, you can define a service-charge using a single method to handle ::premium:
| (defmulti service-charge (fn [acct] [(account-level acct) (:tag acct)])) |
| (defmethod service-charge [::acc/basic ::acc/checking] [_] 25) |
| (defmethod service-charge [::acc/basic ::acc/savings] [_] 10) |
| (defmethod service-charge [::acc/premium ::acc/account] [_] 0) |
At first glance, you may think that derive and isa? duplicate functionality that’s already available to Clojure via Java inheritance. This is not the case. Java inheritance relationships are forever fixed at the moment you define a class. derived relationships can be created when you need them and can be applied to existing objects without their knowledge or consent. So when you discover a useful relationship between existing objects, you can derive that relationship without touching the original objects’ source code and without creating tiresome “wrapper” classes.
If the number of different ways you might define a multimethod has your head spinning, don’t worry. In practice, most Clojure code uses multimethods sparingly. Let’s take a look at some open source Clojure code to get a better idea of how multimethods are used.