Defining Multimethods

Multimethods capture the same pattern we explored in the previous section but support adding new cases without changing the existing code. Multimethods also provide some additional features that we’ll look at later on. Multimethods consist of two parts: a dispatch function (created with defmulti) and a set of methods (created with defmethod).

To define a multimethod, use defmulti:

 (​defmulti​ name dispatch-fn)

name is the name of the new multimethod, and Clojure will invoke dispatch-fn against the method arguments to select one particular method (implementation) of the multimethod.

Consider my-print from the previous section. It takes a single argument, the thing to be printed, and you want to select a specific implementation based on the type of that argument. So dispatch-fn needs to be a function of one argument that returns the type of that argument. Clojure has a built-in function matching this description, namely, class. Use class to create a multimethod called my-print:

 (​defmulti​ my-print class)

At this point, you’ve provided a description of how the multimethod will select a specific method but no actual specific methods. Unsurprisingly, attempts to call my-print will fail:

 (my-println ​"foo"​)
 -> java.lang.IllegalArgumentException​:​ ​
 No method ​for​ dispatch value

To add a specific method implementation to my-println, use defmethod:

 (​defmethod​ name dispatch-val & fn-tail)

name is the name of the multimethod to which an implementation belongs. Clojure matches the result of defmulti’s dispatch function with dispatch-val to select a method, and fn-tail contains arguments and body forms just like a normal function.

Create a my-print implementation that matches on strings:

 (​defmethod​ my-print String [s]
  (.write *out* s))

Now, call my-println with a string argument:

 (my-println ​"stu"​)
 | stu
 -> nil

Next, create a my-print that matches on nil:

 (​defmethod​ my-print nil [s]
  (.write *out* ​"nil"​))

Notice that you’ve solved the problem raised in the previous section. Instead of being joined in a big cond, each implementation of my-println is separate. Methods of a multimethod can live anywhere in your source, and you can add new ones any time, without having to touch the original code.

Dispatch Is Inheritance-Aware

Multimethod dispatch knows about Java inheritance. To see this, create a my-print that handles Number by printing a number’s toString representation:

 (​defmethod​ my-print Number [n]
  (.write *out* (.toString n)))

Test the Number implementation with an integer:

 (my-println 42)
 | 42
 -> nil

42 is a Long, not a Number. Multimethod dispatch is smart enough to know that a long is a number and match anyway. Internally, dispatch uses the isa? function:

 (isa? child parent)

isa? knows about Java inheritance, so it knows that an Integer is a Number:

 (isa? Long Number)
 -> true

isa? is not limited to inheritance. Its behavior can be extended dynamically at runtime, as you will see later in Creating Ad Hoc Taxonomies.

Multimethod Defaults

It would be nice if my-print could have a fallback representation that you could use for any type you haven’t specifically defined. You can use :default as a dispatch value to handle any methods that don’t match anything more specific. Using :default, create a my-println that prints the Java toString value of objects, wrapped in #<>:

 (​defmethod​ my-print :default [s]
  (.write *out* ​"#<"​)
  (.write *out* (.toString s))
  (.write *out* ​">"​))

Now test that my-println prints random things, using the default method:

 (my-println (java.sql.Date. 0))
 -> #<1969-12-31>
 
 (my-println (java.util.Random.))
 -> #<java.util.Random@1c398896>

In the unlikely event that :default already has some specific meaning in your domain, you can create a multimethod using this alternate signature:

 (​defmulti​ name dispatch-fn :default default-value)

The default-value lets you specify your own default. Maybe you’d like to call it :everything-else:

 (​defmulti​ my-print class :default :everything-else)
 (​defmethod​ my-print String [s]
  (.write *out* s))
 (​defmethod​ my-print :everything-else [_]
  (.write *out* ​"Not implemented yet..."​))

Any dispatch value that does not otherwise match will now match against :everything-else.

Dispatching a multimethod on the type of the first argument, as you’ve done with my-print, is by far the most common kind of dispatch. In many object-oriented languages, in fact, it’s the only kind of dynamic dispatch, and it goes by the name polymorphism.

Clojure’s dispatch is much more general. Let’s add a few complexities to my-print and move beyond what’s possible with plain ol’ polymorphism.

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

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