- •Contents
- •Foreword
- •Acknowledgments
- •Preface
- •Who This Book Is For
- •What Is in This Book
- •How to Read This Book
- •Downloading Sample Code
- •Notation Conventions
- •Web Resources and Feedback
- •Getting Started
- •Why Clojure?
- •Clojure Coding Quick Start
- •Exploring Clojure Libraries
- •Introducing Lancet
- •Wrapping Up
- •Exploring Clojure
- •Forms
- •Reader Macros
- •Functions
- •Vars, Bindings, and Namespaces
- •Flow Control
- •Metadata
- •Wrapping Up
- •Working with Java
- •Calling Java
- •Optimizing for Performance
- •Creating and Compiling Java Classes in Clojure
- •Exception Handling
- •Adding Ant Projects and Tasks to Lancet
- •Wrapping Up
- •Unifying Data with Sequences
- •Everything Is a Sequence
- •Using the Sequence Library
- •Clojure Makes Java Seq-able
- •Adding Properties to Lancet Tasks
- •Wrapping Up
- •Functional Programming
- •Functional Programming Concepts
- •How to Be Lazy
- •Lazier Than Lazy
- •Recursion Revisited
- •Wrapping Up
- •Concurrency
- •The Problem with Locks
- •Refs and Software Transactional Memory
- •Use Atoms for Uncoordinated, Synchronous Updates
- •Use Agents for Asynchronous Updates
- •Managing Per-Thread State with Vars
- •A Clojure Snake
- •Making Lancet Targets Run Only Once
- •Wrapping Up
- •Macros
- •When to Use Macros
- •Writing a Control Flow Macro
- •Making Macros Simpler
- •Taxonomy of Macros
- •Making a Lancet DSL
- •Wrapping Up
- •Multimethods
- •Living Without Multimethods
- •Moving Beyond Simple Dispatch
- •Creating Ad Hoc Taxonomies
- •When Should I Use Multimethods?
- •Adding Type Coercions to Lancet
- •Wrapping Up
- •Clojure in the Wild
- •Automating Tests
- •Data Access
- •Web Development
- •Farewell
- •Editor Support
- •Bibliography
- •Index
- •Symbols
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)
