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

ADDING PROPER TIES TO LANCET TASKS 141

If the key names in the two relations do not match, you can pass a keymap that maps the key names in relation-1 to their corresponding keys in relation-2. For example, you can join composers, which use :country, to nations, which use :nation. For example:

(join composers nations {:country :nation})

#{{:language "German", :nation "Austria",

:composer "W. A. Mozart", :country "Austria"} {:language "German", :nation "Germany",

:composer "J. S. Bach", :country "Germany"} {:language "Italian", :nation "Italy",

:composer "Giuseppe Verdi", :country "Italy"}}

You can combine the relational primitives. Perhaps you want to know the set of all countries that are home to the composer of a requiem. You can use select to find all the requiems, join them with their composers, and project to narrow the results to just the country names:

(project

(join

(select #(= (:name %) "Requiem") compositions) composers)

[:country])

#{{:country "Italy"} {:country "Austria"}}

The analogy between Clojure’s relational algebra and a relational database is instructive. Remember, though, that Clojure’s relational algebra is a general-purpose tool. You can use it on any kind of set-relational data. And while you are using it, you also have the entire power of Clojure and Java at your disposal. Next, let’s use the sequence library to improve Lancet’s support for properties on Ant tasks.

4.6 Adding Properties to Lancet Tasks

This section continues the example begun in Section 3.5, Adding Ant Projects and Tasks to Lancet, on page 105. You will need to start with the completed code from that previous section, which you can get from Section 3.5, Lancet Step 1: Ant Projects and Tasks, on page 110.

In step 1, you created an instantiate-task function to automate the instantiation and setup of Ant tasks. One thing that instantiate-task does not yet do is set any properties on an Ant task. You have to call setters in a separate step, such as calling setMessage:

(def echo-task (instantiate-task ant-project "echo"))

#'user/echo-task

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING PROPER TIES TO LANCET TASKS 142

(.setMessage echo-task "some message")

nil

In this section, you will improve instantiate-task to take an additional argument, a map of properties. That way, you can create a new task in a single form:

; TODO: enable this signature

(instantiate-task ant-project "echo" {:message "hello"})

To handle properties in a generic way, you will need to reflect against the available properties of a Java object. Java provides this information via a reflective helper class called the Introspector. From the REPL, import the Introspector, and use it to getBeanInfo on the Echo class:

(import '(java.beans Introspector)) (Introspector/getBeanInfo (class echo-task))

#<java.beans.GenericBeanInfo@d506900>

So far so good. The GenericBeanInfo instance has a getPropertyDescriptors that will return property information for every available property. Using the *1 special variable, dig into the previous result, and pull out the property descriptors:

(.getPropertyDescriptors *1)

#<[Ljava.beans.PropertyDescriptor;@4c3b55a5>

The [L prefix is the Java toString( ) form for an array, so now you have something seq-able to work with. You are going to use this object several times, so stuff it into a var named prop-descs:

(def prop-descs *1)

#'user/prop-descs

Now you have all of the sequence library at your disposal to explore the sequence. Use count to find out how many properties an echo task has:

(count prop-descs)

13

Use first and bean to examine the first property descriptor in more detail:

; output reformatted and elided (bean (first prop-descs))

{:class #=java.beans.PropertyDescriptor,

:writeMethod

...,

:name

...,

:displayName

...,

:constrained

...,

:propertyEditorClass

...,

:readMethod

...,

:preferred

...,

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING PROPER TIES TO LANCET TASKS 143

:expert

...,

:hidden

...,

:propertyType

...,

:bound

...,

:shortDescription

...}

Most of the bean properties are rarely used, and in this section Lancet will need only name (to find a property) and writeMethod (to set it).

Using the Introspector API, plus the sequence library’s filter function, you can write a property-descriptor function that takes a Java instance and a property name and returns a property descriptor:

Download lancet/step_2_repl.clj

(import '(java.beans Introspector))

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

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

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

I prefer to use keywords for names, so the call to (name prop-name) converts a prop-name keyword to a string, which is what Java will expect. The filter finds only those property descriptors whose name matches prop-name. There can be only one such property, since Java objects cannot have two properties with the same name. Test property-descriptor against echo’s message property:

(bean (property-descriptor echo-task :message))

{:class #=java.beans.PropertyDescriptor,

:writeMethod #<public void Echo.setMessage(java.lang.String)>, :name "message",

... lots of other properties ...}

With property-descriptor in place, it is easy work to create a set-property! function that sets a property to a new value:

Download lancet/step_2_repl.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]))))

Line 3 binds pd to a property descriptor, calling the property-descriptor you wrote previously. Line 4 provides a helpful error message if the property does not exist.

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING PROPER TIES TO LANCET TASKS 144

Line 5 invokes the write method for the property. Because Java’s method invoke requires a Java array, not a Clojure sequence, you use intoarray to perform a conversion.

Test that you can actually set echo-task’s message property:

(set-property! echo-task :message "a new message!")

nil

(.execute echo-task)

| [echo] a new message!

nil

Now you can build a set-properties! that takes a map of property name/ property value pairs and invokes set-property! once for each pair. But hold on for just a second. All this talk of invocation sounds very much like the mutable, imperative world of Java, not the immutable, functional world of Clojure. That is because you are interoperating with Ant, which is a mutable, imperative Java API, full of side effects.

Clojure forms that deal with side effects are often prefixed with do, and in fact there is a do-family macro made to order for this situation: doseq:

(doseq bindings & body)

doseq repeatedly executes its body, with the same bindings and filtering as a list comprehension. Using doseq, implement set-properties! thusly:

Download lancet/step_2_repl.clj

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

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

Notice how destructuring simplifies this function definition by allowing you to destructure each key/value pair directly into bindings k and v.

Test set-properties!, using your old friend echo-task:

(set-properties! echo-task {:message "yet another message"})

nil

(.execute echo-task)

| [echo] yet another message

nil

With set-properties! in place, you are now ready to enhance the instantiatetask function you wrote in Section 3.5, Adding Ant Projects and Tasks to Lancet, on page 105.

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

ADDING PROPER TIES TO LANCET TASKS 145

Create a new version of instantiate-task that takes an additional props argument and uses it to set the task’s properties:

Download lancet/step_2_repl.clj

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

task))

To test instantiate-task, create a new var echo-with-msg that binds to a fully configured echo, and verify that it executes correctly:

(def echo-with-msg

(instantiate-task ant-project "echo" {:message "hello"}))

#'user/echo-with-msg

(.execute echo-with-msg) | [echo] hello

nil

The completed code for this section is listed at the very end of the chapter. Note that many of the functions you built in this section are not Ant-specific. property-descriptor, set-property!, and set-properties! are generic and can work with any Java object model. There are plenty of Java libraries other than Ant suffering from tedious XML configuration. Using the generic code you have written here, be an open source hero and give a Java library a Clojure DSL that is easier to use.

Lancet Step 2: Setting Properties

Download lancet/step_2_complete.clj

(ns lancet.step-2-complete

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

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

Prepared exclusively for WG Custom Motorcycles

Report erratum

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