When Should I Use Multimethods?

Multimethods are extremely flexible, and with that flexibility comes choices. How should you choose when to use multimethods, as opposed to some other technique? We approach this question from two directions, asking the following:

  • Where do Clojure projects use multimethods?
  • Where do Clojure projects eschew multimethods?

The most striking thing is that multimethods are rare—about one per 1,000 lines of code. So don’t worry that you’re missing something important if you build a Clojure application with few, or no, multimethods. A Clojure program that defines no multimethods isn’t nearly as odd as an object-oriented program with no polymorphism.

Many multimethods dispatch on class. Dispatch-by-class is the easiest kind of dispatch to understand and implement. We already covered it in detail with the my-print example, so we’ll say no more about it here.

Clojure multimethods that dispatch on something other than class are fairly rare. We can look directly in Clojure for some examples. The clojure.inspector and clojure.test libraries use unusual dispatch functions.

The Inspector

Clojure’s inspector library uses Swing to create simple views of data. You can use it to get a tree view of your system properties:

 (require '[clojure.inspector :refer [inspect inspect-tree]])
 (inspect-tree (System/getProperties))

inspect-tree returns (and displays) a JFrame with a tree view of anything that’s treeish. So you could also pass a nested map to inspect-tree:

 (inspect-tree {:clojure {:creator ​"Rich"​ :runs-on-jvm true}})

Treeish things are made up of nodes that can answer two questions:

  • Who are my children?
  • Am I a leaf node?

The treeish concepts of “tree,” “node,” and “leaf” all sound like candidates for classes or interfaces in an object-oriented design. But the inspector doesn’t work this way. Instead, it adds a “treeish” type system in an ad hoc way to existing types, using a dispatch function named collection-tag:

 ; from Clojure's clojure/inspector.clj
 (​defn​ collection-tag [x]
  (​cond
  (map-entry? x) :entry
  (instance? java.util.Map x) :seqable
  (instance? java.util.Set x) :seqable
  (sequential? x) :seq
  (instance? clojure.lang.Seqable x) :seqable
  :else :atom))

collection-tag returns one of the keywords :entry, :map, :seqable, :seq, or :atom. These act as the type system for the treeish world. The collection-tag function is then used to dispatch three different multimethods that select specific implementations based on the treeish type system.

 (​defmulti​ is-leaf collection-tag)
 
 (​defmulti​ get-child
  (​fn​ [parent index] (collection-tag parent)))
 
 (​defmulti​ get-child-count collection-tag)
 ; method implementations elided for brevity

The treeish type system is added around the existing Java type system. Existing objects don’t have to do anything to become treeish; the inspector library does it for them. Treeish demonstrates a powerful style of reuse. You can discover new type relationships in existing code and take advantage of these relationships simply, without having to modify the original code.

clojure.test

The clojure.test library in Clojure lets you write several different kinds of assertions using the is macro. You can assert that arbitrary functions are true. For example, 10 is not a string:

 (require '[clojure.test :refer [is]])
 (is (string? 10))
 
 FAIL in () (NO_SOURCE_FILE:2)
 expected​:​ (string? 10)
 actual​:​ (not (string? 10))
 -> false

Although you can use an arbitrary function, is knows about a few specific functions and provides more detailed error messages. For example, you can check that a string is not an instance? of Collection:

 (is (instance? java.util.Collection ​"foo"​))
 
 FAIL in () (NO_SOURCE_FILE:3)
 expected​:​ (instance? java.util.Collection ​"foo"​)
 actual​:​ java.lang.String
 -> false

is also knows about =. Verify that power does not equal wisdom.

 (is (= ​"power"​ ​"wisdom"​))
 
 FAIL in () (NO_SOURCE_FILE:4)
 expected​:​ (= ​"power"​ ​"wisdom"​)
 actual​:​ (not (= ​"power"​ ​"wisdom"​))
 -> false

Internally, is uses a multimethod named assert-expr, which dispatches not on the type but on the actual identity of its first argument:

 (​defmulti​ assert-expr (​fn​ [form message] (first form)))

Since the first argument is a symbol representing what function to check, this amounts to yet another ad hoc type system. This time, there are three types: =, instance?, and everything else.

The various assert-expr methods add specific error messages associated with different functions you might call from is. Because multimethods are open ended, you can add your own assert-expr methods with improved error messages for other functions you frequently pass to is.

Counterexamples

As you saw in the previous section, you can often use multimethods to hoist branches that are based on type out of the main flow of your functions. To find counterexamples where multimethods should not be used, we looked through Clojure’s core to find type branches that had not been hoisted to multimethods.

A simple example is Clojure’s class, which is a null-safe wrapper for the underlying Java getClass. Minus comments and metadata, class is as follows:

 (​defn​ class [x]
  (​if​ (nil? x) x (.getClass x)))

You could write a version of class as a multimethod by dispatching on identity:

 (​defmulti​ my-class identity)
 (​defmethod​ my-class nil [_] nil)
 (​defmethod​ my-class :default [x] (.getClass x))

Any nil-check could be rewritten this way. But we find the original class function easier to read than the multimethod version. This is a nice “exception that proves the rule.” Even though class branches on type, the branching version is easier to read.

Use the following general rules when deciding whether to create a function or a multimethod:

  • If a function branches based on a type, or multiple types, consider a multimethod.

  • Types are whatever you discover them to be. They do not have to be explicit Java classes or data tags.

  • You should be able to interpret the dispatch value of a defmethod without having to refer to the defmulti.

  • Don’t use multimethods merely to handle optional arguments or recursion.

When in doubt, try writing the function in both styles and pick the one that seems more readable.

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

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