In this section, we’ll solve the two issues mentioned in the previous section. First, you don’t need the conceptual or syntactic complexity of full multimethods when you only want to dispatch on the class of the first argument. Second, you want to group related multimethods together so they read better.
We’ll call the solution to this modus operandi, which is a Latin phrase that means
“method of operating.” The name reflects our intention here, which is to describe a set of operating procedures for something.
9.2.1 def-modus-operandi
Let’s start with the code you’d like to be able to write:
(def-modus-operandi ExpenseCalculations (total-cents [e])
(is-category? [e category]))
What you’re saying here is that you’re defining a modus operandi called Expense- Calculations that will consist of two methods, namely total-cents and is-category?. You won’t specify the dispatch function as you did before, because you always want it to be the class of the first argument of each method. In this case, both methods will dispatch based on the class of the expense object, be it a Clojure map or the Java Expense class or any other data type you end up supporting.
Now, let’s look at implementing it. As you can imagine, def-modus-operandi is a macro. Here’s the code along with a couple of associated helper functions to make the code easier to read:
(defn dispatch-fn-for [method-args]
`(fn ~method-args (class ~(first method-args)))) (defn expand-spec [[method-name method-args]]
`(defmulti ~method-name ~(dispatch-fn-for method-args))) (defmacro def-modus-operandi [mo-name & specs]
`(do
~@(map expand-spec specs)))
So all you’re doing is generating code that creates multimethods. Here’s what the expanded version looks like:
(do
(clojure.core/defmulti total-cents (clojure.core/fn [e]
(clojure.core/class e))) (clojure.core/defmulti is-category? (clojure.core/fn [e category]
(clojure.core/class e))))
Notice that the expanded form of is-category? is the same as when you wrote it by hand earlier. The expansion for total-cents is slightly different, only because you can generate the same dispatch function no matter how many arguments the func- tion takes.
Now that you have a way to specify the methods in your modus operandi, you need a way to detail it for the types you’d like to support. We’ll do that next.
9.2.2 detail-modus-operandi
After defining the what of a modus operandi, you need to define the how. You’ll create a new macro called detail-modus-operandi that you’ll use in the following manner:
(detail-modus-operandi ExpenseCalculations clojure.lang.IPersistentMap
(total-cents [e]
(-> (:amount-dollars e) (* 100)
(+ (:amount-cents e)))) (is-category? [e some-category]
(= (:category e) some-category)))
Most of the code should be familiar to you, because it’s nearly identical to the code from the previous section. Because all the methods are being defined for the same dis- patch value, you’ve made it so that you have to specify it only once. Here’s the imple- mentation of the macro, along with an associated helper function:
(defn expand-method [data-type [name & body]]
`(defmethod ~name ~data-type ~@body))
(defmacro detail-modus-operandi [mo-name data-type & fns]
`(do
~@(map #(expand-method data-type %) fns)))
The expansion of this call to detail-modus-operandi is as follows:
(do
(clojure.core/defmethod total-cents clojure.lang.IPersistentMap [e]
(-> (:amount-dollars e) (* 100)
(+ (:amount-cents e)))) (clojure.core/defmethod is-category?
clojure.lang.IPersistentMap [e some-category]
(= (:category e) some-category)))
227 Examining the operations side of the expression problem
So you’ve done what you set out to do. You have a new abstraction that sits atop multi- methods that behave like subtype polymorphism. The methods dispatch on the type of the first argument.
Notice that even though you specified the name of your modus operandi here (called ExpenseCalculations), you haven’t used it for anything. You can make your modus operandi more useful if you use the named objects to track such things as what it contains and who implements it. Let’s do that next.
9.2.3 Tracking your modus operandi
So far, you’ve allowed declarations of a modus operandi that’s a set of related multi- methods that dispatches on the type of the first argument. In this section, you’ll collect some meta-information about these methods that you can use to programmatically query things about the modus operandi.
DURINGDEF-MODUS-OPERANDI
The first thing you’ll do is define a var with the name of the modus operandi. Doing that by itself is easy enough: you add a call to def in the def-modus-operandi macro.
The question is what should the var be bound to? A simple option is to create a map containing information about the modus operandi. Try that approach:
(defmacro def-modus-operandi [mo-name & specs]
`(do
(def ~mo-name ~(mo-methods-registration specs)) ~@(map expand-spec specs)))
You’ve delegated to a helper function called mo-methods-registration, so imple- ment that next:
(defn mo-method-info [[name args]]
{(keyword name) {:args `(quote ~args)}}) (defn mo-methods-registration [specs]
(apply merge (map mo-method-info specs)))
You’re collecting the name and arguments of each method into a map. This map, with all the information about the methods being specified as part of the modus operandi, will become the root binding of a var by the same name as the modus operandi. You can try it. First, you’ll redefine the modus operandi:
(def-modus-operandi ExpenseCalculations (total-cents [e])
(is-category? [e category]))
;=> #'user/is-category?
Next, see what the ExpenseCalculations var is bound to:
ExpenseCalculations
;=> {:is-category? {:args [e category]}, :total-cents {:args [e]}}
So you have the basic information. Next, you’ll collect some more information every time detail-modus-operandi is called.
DURINGDETAIL-MODUS-OPERANDI
To collect information of the implementer of a modus operandi, you’ll first need to pass the modus operandi into the expand-method function:
(defmacro detail-modus-operandi [mo-name data-type & fns]
`(do
~@(map #(expand-method mo-name data-type %) fns)))
Now that the expand-method knows which modus operandi it’s going to create a method for, you can collect information about it:
(defn expand-method [mo-name data-type [method-name & body]]
`(do
(alter-var-root (var ~mo-name) update-in
[(keyword '~method-name) :implementors] conj ~data-type) (defmethod ~method-name ~data-type ~@body)))
To better understand this addition to the expand-method function, let’s talk about the data you’re collecting. Recall that the modus operandi var is bound to a map that con- tains a key for each method. The value for each such key is another map. The only key in the inner map so far is :args, and to collect the data types of the implementors to this map, you’ll introduce another key called :implementors. So here you’re going to conj the data type onto the list of implementors (if any) each time a method of a modus operandi is implemented.
Finally, let’s look at the function alter-var-root. Here’s the doc string:
(doc alter-var-root) --- clojure.core/alter-var-root ([v f & args])
Atomically alters the root binding of var v by applying f to its current value plus any args
So you’re passing it the var for the modus operandi and the function update-in. The arguments to update-in are a sequence of keys that locates a nested value and a function that will be applied to the existing value along with any other arguments.
In this case, update-in is passed the function conj along with the data type you’d like recorded.
Phew, that’s a lot of work for a single line of code! The following listing shows the complete implementation of modus operandi in a single namespace.
(ns clj-in-act.ch9.modus-operandi) (defn dispatch-fn-for [method-args]
`(fn ~method-args (class ~(first method-args))))
Listing 9.2 Implementing modus operandi on top of multimethods
229 Examining the operations side of the expression problem
(defn expand-spec [[method-name method-args]]
`(defmulti ~method-name ~(dispatch-fn-for method-args))) (defn mo-method-info [[name args]]
{(keyword name) {:args `(quote ~args)}}) (defn mo-methods-registration [specs]
(apply merge (map mo-method-info specs))) (defmacro def-modus-operandi [mo-name & specs]
`(do
(def ~mo-name ~(mo-methods-registration specs)) ~@(map expand-spec specs)))
(defn expand-method [mo-name data-type [method-name & body]]
`(do
(alter-var-root (var ~mo-name) update-in [(keyword '~method-name) :implementors] conj ~data-type)
(defmethod ~method-name ~data-type ~@body)))
(defmacro detail-modus-operandi [mo-name data-type & fns]
`(do
~@(map #(expand-method mo-name data-type %) fns)))
Let’s look at it in action. First, make a call to detail-modus-operandi:
(detail-modus-operandi ExpenseCalculations clojure.lang.IPersistentMap
(total-cents [e]
(-> (:amount-dollars e) (* 100)
(+ (:amount-cents e)))) (is-category? [e some-category]
(= (:category e) some-category)))
;=> #<MultiFn clojure.lang.MultiFn@4aad8dbc>
Now look at the ExpenseCalculations var:
ExpenseCalculations
;=> {:is-category? {:implementors (clojure.lang.IPersistentMap), :args [e category]},
:total-cents {:implementors (clojure.lang.IPersistentMap), :args [e]}}
As you can see, you’ve added the new :implementors key to the inner maps, and they have a value that’s a sequence of the implementors so far. Now implement the modus operandi for the Java Expense class:
(detail-modus-operandi ExpenseCalculations com.curry.expenses.Expense
(total-cents [e]
(.amountInCents e))
(is-category? [e some-category]
(= (.getCategory e) some-category)))
;=> #<MultiFn clojure.lang.MultiFn@4aad8dbc>
You can now see what the ExpenseCalculations var is bound to:
ExpenseCalculations
;=> {:is-category? {:implementors (com.curry.expenses.Expense clojure.lang.IPersistentMap), :args [e category]},
:total-cents {:implementors (com.curry.expenses.Expense clojure.lang.IPersistentMap), :args [e]}}
And there you have it: you’re collecting a sequence of all implementing classes inside the map bound to the modus-operandi var. You should now ensure that everything still works with the original code. Figure 9.1 shows a conceptual view of the process of defining a modus operandi and then detailing it. Listing 9.3 shows the complete code for the expense namespace.
(ns clj-in-act.ch9.expense-modus-operandi
(:require [clj-in-act.ch9.modus-operandi :refer :all]) (:import [java.text SimpleDateFormat]
[java.util Calendar]))
Listing 9.3 The expense namespace using the modus operandi multimethod syntax
def-modus-operandi Module
design detail-modus-operandi
Updated var modus-operandi modus-operandivar
(for introspection)
After expansion of detail-modus-operandi After expansion of
def-modus-operandi
defmulti defmethod
Figure 9.1 Calling def-modus-operandi creates a var that will hold information about the modus operandi that can later be used to introspect it. The macro itself makes as many calls to defmulti as needed. The detail-modus-operandi macro is the other side of the modus operandi concept: it fills out the implementation details by expanding to as many defmethod calls as specified. It also updates the modus-operandi var to reflect the implementor information.
231 Examining the operations side of the expression problem
(defn new-expense [date-string dollars cents category merchant-name]
(let [calendar-date (Calendar/getInstance)]
(.setTime calendar-date
(.parse (SimpleDateFormat. "yyyy-MM-dd") date-string)) {:date calendar-date
:amount-dollars dollars :amount-cents cents :category category
:merchant-name merchant-name})) (def-modus-operandi ExpenseCalculations (total-cents [e])
(is-category? [e category]))
(detail-modus-operandi ExpenseCalculations clojure.lang.IPersistentMap
(total-cents [e]
(-> (:amount-dollars e) (* 100)
(+ (:amount-cents e)))) (is-category? [e some-category]
(= (:category e) some-category))) (detail-modus-operandi ExpenseCalculations com.curry.expenses.Expense
(total-cents [e]
(.amountInCents e))
(is-category? [e some-category]
(= (.getCategory e) some-category))) (defn category-is [category]
#(is-category? % category)) (defn total-amount
([expenses-list]
(total-amount (constantly true) expenses-list)) ([pred expenses-list]
(->> expenses-list (filter pred) (map total-cents) (apply +))))
Similarly, the following listing shows the tests you’ve written so far, all in one place.
You’ll run the tests next.
(ns clj-in-act.ch9.expense-test
(:import [com.curry.expenses Expense])
(:require [clj-in-act.ch9.expense-modus-operandi :refer :all]
[clojure.test :refer :all]))
(def clj-expenses [(new-expense "2009-8-20" 21 95 "books" "amazon.com") (new-expense "2009-8-21" 72 43 "food" "mollie-stones") (new-expense "2009-8-22" 315 71 "car-rental" "avis") (new-expense "2009-8-23" 15 68 "books" "borders")]) (deftest test-clj-expenses-total
(is (= 42577 (total-amount clj-expenses)))
(is (= 3763 (total-amount (category-is "books") clj-expenses))))
Listing 9.4 Testing the implementation of modus operandi calculating expense totals
(def java-expenses [(Expense. "2009-8-24" 44 95 "books" "amazon.com") (Expense. "2009-8-25" 29 11 "gas" "shell")]) (deftest test-java-expenses-total
(let [total-cents (map #(.amountInCents %) java-expenses)]
(is (= 7406 (apply + total-cents)))))
(def mixed-expenses (concat clj-expenses java-expenses)) (deftest test-mixed-expenses-total
(is (= 49983 (total-amount mixed-expenses)))
(is (= 8258 (total-amount (category-is "books") mixed-expenses))))
These tests now all pass:
(use 'clojure.test) (run-tests 'clj-in-act.ch9.expense-test) Testing clj-in-act.ch9.expense-test
Ran 3 tests containing 5 assertions.
0 failures, 0 errors.
;=> {:type :summary, :test 3, :pass 5, :fail 0, :error 0}
Finally, before wrapping up this section, you’ll write a couple of functions that will make it easy to query data about your modus operandi, like ExpenseCalculations.
QUERYINGMODUSOPERANDI
The first function you’ll write discerns what data types implement a particular modus operandi. Consider this code:
(defn implementors [modus-operandi method]
(get-in modus-operandi [method :implementors]))
And this allows you to do things like this:
(implementors ExpenseCalculations :is-category?)
;=> (com.curry.expenses.Expense clojure.lang.IPersistentMap)
Now you’ll write another function that when given a class of a particular data type can tell you if it implements a particular method of a modus operandi. Here’s the code:
(defn implements? [implementor modus-operandi method]
(some #{implementor} (implementors modus-operandi method)))
Now test it at the REPL:
(implements? com.curry.expenses.Expense ExpenseCalculations :is-category?)
;=> com.curry.expenses.Expense
Note that implements? returns the class itself, which is truthy. Here’s a negative scenario:
(implements? java.util.Date ExpenseCalculations :is-category?)
;=> nil
233 Examining the operations side of the expression problem
Now that you have a function such as implements?, you can also write a broader func- tion to see if a class implements a modus operandi completely:
(defn full-implementor? [implementor modus-operandi]
(->> (keys modus-operandi)
(map #(implements? implementor modus-operandi %)) (not-any? nil?)))
Here it is in action:
(full-implementor? com.curry.expenses.Expense ExpenseCalculations)
;=> true
To test the negative side, you’ll partially implement the modus operandi:
(detail-modus-operandi ExpenseCalculations java.util.Date
(total-cents [e]
(rand-int 1000)))
;=> #<MultiFn clojure.lang.MultiFn@746ac18c>
And now you can test what you were after:
(full-implementor? java.util.Date ExpenseCalculations)
;=> false
You can implement other functions such as these, because the value bound to the modus-operandi var is a regular map that can be inspected like any other. Next, let’s examine the downsides to the modus operandi approach to the expression problem.
9.2.4 Error handling and trouble spots in this solution
In this section, you took multimethods and wrote a little DSL on top of them that allows you to write simpler, clearer code when you want to dispatch on the class of the first argument. You were also able to group related multimethods together via this new syntax, and this allowed the code to be self-documenting by communicating that certain multimethods are related to each other.
What we haven’t touched on at all is error handling. For instance, if you eval the same detail-modus-operandi calls multiple times, the data-collection functions would add the class to your modus operandi metadata map multiple times. It’s an easy fix, but this isn’t the most robust code in the world, because it was written to demon- strate the abstraction.
There are other trouble spots as well. For instance, because you built this on top of multimethods, and multimethods support hierarchies (and Java inheritance hierar- chies by default), the implements? and related functions won’t give accurate answers as they stand now.
Further, because this is such a bare-bones implementation, many other features might be desirable in a more production-ready version. The other downside is that
there’s a small performance hit when using multimethods because they have to call the dispatch function and then match the dispatch value against available multi- methods. After all, this approach is syntactic sugar on top of multimethods.
In the next section, you’ll see Clojure’s version of the solution.