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

ADDING TYPE COERCIONS TO LANCET 259

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

Download examples/multimethods.clj

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

•Do not 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.

As luck would have it, the Lancet sample application needs a multi- method—and not just a boring single dispatch either. Lancet needs an oddball function that dispatches on the class of two different arguments. Let’s go build it.

8.6 Adding Type Coercions to Lancet

In Section 3.5, Adding Ant Projects and Tasks to Lancet, on page 105, I glossed over an irritant in Lancet’s Ant integration. When you create an Ant task, you have to know the types of any properties used to configure the task. For example, the dir property of mkdir is not a String:

(.setDir mkdir-task "sample-dir")

java.lang.ClassCastException

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING TYPE COERCIONS TO LANCET 260

Instead, you have to know that the dir property is a File:

(.setDir mkdir-task (java.io.File. "sample-dir"))

nil

If you spend time trying out various other Ant tasks in Lancet, you will see that the problem is serious. Furthermore, Ant does not have this problem, even though Ant task properties usually begin their life as strings in an XML document. Ant must know how to convert strings into other types on demand. Lancet will need to do something similar.

Let’s create a generic coercion system for Lancet. To coerce an argument from one type to another, you need to know two things: the type of the source and the type of the destination. Create a coerce multimethod that dispatches on a destination class and a source instance:

Download lancet/step_5_repl.clj

(defmulti coerce

(fn [dest-class src-inst] [dest-class (class src-inst)]))

The destination is listed first for consistency with Clojure functions like cast.

Now, you can write a coerce method that coerces Strings to Files:

Download lancet/step_5_repl.clj

(defmethod coerce [java.io.File String] [_ str] (java.io.File. str))

This is very clean. With almost no noise, it says to the reader: “in order to make a File from a String, you must call the File constructor.” Make sure that it works:

(coerce java.io.File "foo")

#<File foo>

Not every coercion will be so simple. For example, Ant has its own notion of truth. The strings "on", "yes", and "true" are all true, regardless of case. Create a coercion that enforces this:

Download lancet/step_5_repl.clj

(defmethod coerce [Boolean/TYPE String] [_ str] (contains? #{"on" "yes" "true" } (.toLowerCase str)))

Make sure that it works:

(coerce Boolean/TYPE "no")

false

(coerce Boolean/TYPE "yes")

true

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING TYPE COERCIONS TO LANCET 261

(coerce Boolean/TYPE "TRUE")

true

Even though the last defmethod looks Ant-specific, the mechanism is general. Different applications can mix and match coercions to suit their needs. If truth works a little differently in the valley, no problem:

(defmethod coerce [Boolean/TYPE String] [_ str] (contains? #{"totally" "way" } (.toLowerCase str)))

You are going to want to pass all Ant property setters through the coercion mechanism, so as a last step create a default coercion. This coercion simply verifies that the type is already compatible with the destination by calling Clojure’s cast:

Download lancet/step_5_repl.clj

(defmethod coerce :default [dest-cls obj] (cast dest-cls obj))

Make sure this works by trying a few sensible and insensible coercions:

(coerce Comparable "hello")

"hello"

(coerce java.io.File (java.util.Random.))

java.lang.ClassCastException

Now let’s add coercion support to Lancet’s set-property. You will need to start with the code from Section 4.6, Lancet Step 2: Setting Properties, on page 145. In that code, set-properties is defined as follows:

Download lancet/snippets.clj

Line 1 (use '[clojure.contrib.except :only (throw-if)])

2(defn set-property! [inst prop value]

3(let [pd (property-descriptor inst prop)]

4(throw-if (nil? pd) (str "No such property " prop))

5(.invoke (.getWriteMethod pd) inst (into-array [value]))))

On line 5, you need to coerce the value before invoking the property write method. coerce expects a destination class, but Lancet currently provides a property-writing Method. You can get the destination class from that Method with a little Java reflection:

Download lancet/step_5_repl.clj

(defn get-property-class [write-method] (first (.getParameterTypes write-method)))

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING TYPE COERCIONS TO LANCET

262

Next, enhance set-property! to use coerce.

The expression that calls invoke is getting fairly complex, so introduce a let of write-method and dest-class to make set-property more readable:

Download lancet/step_5_repl.clj

(defn set-property! [inst prop value]

(let [pd (property-descriptor inst prop)]

(throw-if (nil? pd) (str "No such property " prop)) (let [write-method (.getWriteMethod pd)

dest-class (get-property-class write-method)] (.invoke

write-method inst (into-array [(coerce dest-class value)])))))

You can test the coercions by instantiating Ant tasks and passing in plain old strings. For example, create a mkdir task, and pass in dir as a string.

(def mkfoo

(instantiate-task ant-project "mkdir" {:dir "foo" })) -> #'user/mkfoo

(.execute mkfoo)

[mkdir] Created dir:/lancet-test/foo

The completed code for this section is listed in Section 8.6, Lancet Step 5: Type Coercions. As you explore Ant, you may find other coercions, but Ant task integration is now almost complete. Lancet’s Ant integration can now do the following:

•Create any Ant task

•Set arbitrary Java properties

•Define common coercions for property setters

The codebase size for Ant task integration is about fifty lines.

Lancet Step 5: Type Coercions

Download lancet/step_5_complete.clj

(ns lancet.step-5-complete

(:use clojure.contrib.except) (:import (java.beans Introspector)))

(defmulti coerce

(fn [dest-class src-inst] [dest-class (class src-inst)]))

(defmethod coerce [java.io.File String] [_ str] (java.io.File. str))

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING TYPE COERCIONS TO LANCET 263

(defmethod coerce [Boolean/TYPE String] [_ str] (contains? #{"on" "yes" "true" } (.toLowerCase str)))

(defmethod coerce :default [dest-cls obj] (cast dest-cls obj))

(def

#^{:doc "Dummy ant project to keep Ant tasks happy" } ant-project

(let [proj (org.apache.tools.ant.Project.)

logger (org.apache.tools.ant.NoBannerLogger.)] (doto logger

(.setMessageOutputLevel org.apache.tools.ant.Project/MSG_INFO) (.setOutputPrintStream System/out)

(.setErrorPrintStream System/err)) (doto proj

(.init)

(.addBuildListener logger))))

(defn property-descriptor [inst prop-name] (first

(filter #(= (name prop-name) (.getName %)) (.getPropertyDescriptors

(Introspector/getBeanInfo (class inst))))))

(defn get-property-class [write-method] (first (.getParameterTypes write-method)))

(defn set-property! [inst prop value]

(let [pd (property-descriptor inst prop)]

(throw-if (nil? pd) (str "No such property " prop)) (let [write-method (.getWriteMethod pd)

dest-class (get-property-class write-method)] (.invoke

write-method inst (into-array [(coerce dest-class value)])))))

(defn set-properties! [inst prop-map]

(doseq [[k v] prop-map] (set-property! inst k v)))

(defn instantiate-task [project name props] (let [task (.createTask project name)]

(throw-if (nil? task) (str "No task named " name)) (doto task

(.init)

(.setProject project) (set-properties! props))))

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]