Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Clojure.pdf
Скачиваний:
17
Добавлен:
09.05.2015
Размер:
12.92 Mб
Скачать

MOVING BEYOND SIMPLE DISPATCH 249

anything more specific. Using :default, create a my-println that prints the Java toString value of objects, wrapped in #<>:

Download examples/multimethods.clj

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

(.write *out* (.toString s)) (.write *out* ">" ))

Now test that my-println can print any old random thing, 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 would like to call it :everything-else:

Download examples/multimethods/default.clj

(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 have done with my-print, is by far the most common kind of dispatch. In many object-oriented languages, in fact, it is 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 is possible with plain ol’ polymorphism.

8.3 Moving Beyond Simple Dispatch

Clojure’s print function prints various “sequencey” things as lists. If you wanted my-print to do something similar, you could add a method that

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

MOVING BEYOND SIMPLE DISPATCH 250

dispatched on a collection interface high in the Java inheritance hierarchy, such as Collection:

Download examples/multimethods.clj

(use '[clojure.contrib.str-utils :only (str-join)]) (defmethod my-print java.util.Collection [c]

(.write *out* "(" )

(.write *out* (str-join " " c)) (.write *out* ")" ))

Now, try various sequences to see that they get a nice print representation:

(my-println (take 6 (cycle [1 2 3]))) | (1 2 3 1 2 3)

nil

(my-println [1 2 3]) | (1 2 3)

nil

Perfectionist that you are, you cannot stand that vectors print with rounded braces, unlike their literal square-brace syntax. So, add yet another my-print method, this time to handle vectors. Vectors all implement a IPersistentVector, so this should work:

Download examples/multimethods.clj

(defmethod my-print clojure.lang.IPersistentVector [c] (.write *out* "[" )

(.write *out* (str-join " " c)) (.write *out* "]" ))

But it doesn’t work. Instead, printing vectors now throws an exception:

(my-println [1 2 3])

java.lang.IllegalArgumentException: Multiple methods match dispatch value: class clojure.lang.LazilyPersistentVector -> interface clojure.lang.IPersistentVector and

interface java.util.Collection, and neither is preferred

The problem is that two dispatch values now match for vectors: Collection and IPersistentVector. Many languages constrain method dispatch to make sure these conflicts never happen, such as by forbidding multiple inheritance. Clojure takes a different approach. You can create conflicts, and you can resolve them with prefer-method:

(prefer-method multi-name loved-dispatch dissed-dispatch)

When you call prefer-method for a multimethod, you tell it to prefer the loved-dispatch value over the dissed-dispatch value whenever there is a

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

CREATING AD HOC TAXONOMIES 251

conflict. Since you want the vector version of my-print to trump the collection version, tell the multimethod what you want:

Download examples/multimethods.clj

(prefer-method

my-print clojure.lang.IPersistentVector java.util.Collection)

Now, you should be able to route both vectors and other sequences to the correct method implementation:

(my-println (take 6 (cycle [1 2 3]))) | (1 2 3 1 2 3)

nil

(my-println [1 2 3]) | [1 2 3]

nil

Many languages create complex rules, or arbitrary limitations, in order to resolve ambiguities in their systems for dispatching functions. Clojure allows a much simpler approach: Just don’t worry about it! If there is an ambiguity, use prefer-method to resolve it.

8.4 Creating Ad Hoc Taxonomies

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 struct for an account, using a tag to distinguish the two. Place the code in the namespace examples.multimethods.account. To do this, you will need to create a file named examples/multimethods/account.clj on your classpath1 and then enter the following code:

Download examples/multimethods/account.clj

(ns examples.multimethods.account)

(defstruct account :id :tag :balance)

Now, you are going to create two different checking accounts, tagged as ::Checking and ::Savings. The capital names are a Clojure convention

1. Note that the example code for the book includes a completed version of this example, already on the classpath. To work through the example yourself, simply move or rename the completed example to get it out of the way.

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

CREATING AD HOC TAXONOMIES 252

to show the keywords are acting as types. 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 will need to fully qualify them:

(struct account 1 ::examples.multimethods.account/Savings 100M)

{: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 (struct account 1 ::acc/Savings 100M))

#'user/test-savings

(def test-checking (struct account 2 ::acc/Checking 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 for checking accounts is 0 and for savings accounts is 5 percent. Create a multimethod interest-rate that dispatches based on :tag, like so:

Download examples/multimethods/account.clj

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

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

CREATING AD HOC TAXONOMIES 253

(interest-rate test-savings)

0.05M

(interest-rate test-checking)

0M

Accounts have an annual service charge, with rules as follows:

Normal checking accounts pay a $25 service charge.

Normal savings accounts pay a $10 service charge.

Premium accounts have no fee.

Checking accounts with a balance of $5,000 or more are premium.

Savings accounts with a balance of $1,000 or more are premium.

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, even though there is no explicit premium tag on an account. Create an account-level multimethod that returns

::Premium or ::Basic:

Download examples/multimethods/account.clj

(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 (struct account 1 ::acc/Savings 2000M))

:examples.multimethods.account/Premium

(account-level (struct account 1 ::acc/Checking 2000M))

:examples.multimethods.account/Basic

Now you might be tempted to implement service-charge using accountlevel as a dispatch function:

Download examples/multimethods/service_charge_1.clj

; bad approach

(defmulti service-charge account-level) (defmethod service-charge ::Basic [acct]

(if (= (:tag acct) ::Checking) 25 10)) (defmethod service-charge ::Premium [_] 0)

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

CREATING AD HOC TAXONOMIES 254

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 are 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:

Download examples/multimethods/service_charge_2.clj

(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

Notice that the previous tests did not even bother to create a “real” account for testing. Structs like account are simply maps that are optimized for storing particular fields, but nothing stops you from using a plain old map if you find it more convenient.

Adding Inheritance to Ad Hoc Types

There is one further improvement you can make to service-charge. Since all premium accounts have the same service charge, it feels 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:

Download examples/multimethods/service_charge_3.clj

(derive ::acc/Savings ::acc/Account) (derive ::acc/Checking ::acc/Account)

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)