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

MAKING LANCET TARGETS RUN ONLY ONCE 207

6.7 Making Lancet Targets Run Only Once

So far, you have worked on making Lancet able to invoke existing Ant tasks. Now it is time to think about how to implement a Lancet target, that is, a function that runs only once per build. Because we will build targets on top of functions, any Clojure function can become a target.

Targets are what makes Lancet dependency-based. Calling a target is the same as saying “I depend on this target.” Each target should execute only once, no matter how many times it is called. In other words:

1.The first caller of any target should execute the target.

2.All subsequent callers of a target should wait for the first caller to finish and then move on without calling the target again.

In this chapter, you have seen Clojure’s concurrency library: refs, atoms, agents, and vars. Another possibility is to dip down into Java and use the locking or atomic capabilities in Java itself. Let’s now consider each of these in turn to find the best approach for implementing targets.

Refs provide coordinated, synchronous updates. At first glance, this looks like a good fit for targets. But Clojure transactions must be side effect free. Build tasks certainly have side effects, and retrying might be expensive or even incorrect, so rule out refs.

Atoms cannot do the job alone. They provide no coordination and hence no way to wait for the first caller of a task to finish. We will use an atom to remember the return value of each target after it is called.

Agents almost work. Every target could have an agent, and you could send-off the work of the target. Subsequent callers could await the completion of any targets they need. However, you cannot await one agent while in another agent. This prevents possible deadlocks and enforces the idea that agents are not a coordination mechanism. Rule out agents.

Vars would unnecessarily limit Lancet to running on a single thread. Rule out vars.

Another possibility is good old-fashioned locking. It is perfectly fine to use locks in Clojure, if that is what your design calls for. Lancet needs to make coordinated, synchronous updates to impure functions, and this is a job for locks.

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

MAKING LANCET TARGETS RUN ONLY ONCE 208

At the REPL, create a function named create-runonce. This function will take an ordinary function and return a new function that will run only once.

Download lancet/step_3_repl.clj

Line 1 (defn create-runonce [function] 2 (let [sentinel (Object.)

3result (atom sentinel)]

4(fn [& args]

5(locking sentinel

6

(if (= @result

sentinel)

7

(reset! result (function))

8

@result)))))

 

create-runonce works as follows:

Line 2 creates a sentinel object. Whenever you see the sentinel value, you know that the target function has not yet run. The sentinel is a simple Java object so that it will never compare as equal to anything else.

The result atom (line 3) remembers the return value from calling a target. It is initially set to sentinel, which means that the target has not run yet.

Line 5 locks the sentinel. This guarantees that only one thread at a time can execute the code that follows.

If the result is sentinel, then this is the first caller. Line 7 calls the function and reset!s the result.

If the result is not sentinel, then this is not the first caller, so there is nothing to do but return the value of result.

Create a function named println-once that runs println only once, using create-runonce:

(def println-once (create-runonce println))

#'user/println-once

Now for the moment of truth. Call println-once twice.

> (println-once "there can be only one!") | there can be only one!

nil

user=> (println-once "there can be only one!")

nil

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

MAKING LANCET TARGETS RUN ONLY ONCE 209

Just to be on the safe side, verify that the original println still works, no matter how many times you call it:

(println "here") | here

nil

(println "still here") | still here

nil

Now that you have the core code working, let’s make it more usable. It would be nice to have a simple predicate to see whether a target has run. For testing, it would also be useful to be able to reset a target so that it will run again. Create a runonce function that returns three values:

A has-run? predicate

A reset-fn

The runonce function itself

Download lancet/step_3_complete.clj

Line 1 (defn runonce

-"Create a function that will only run once. All other invocations

-return the first calculated value. The function can have side effects.

-Returns a [has-run-predicate, reset-fn, once-fn]"

5 [function]

-(let [sentinel (Object.)

-result (atom sentinel)

-reset-fn (fn [] (reset! result sentinel) nil)

-has-run? #(not= @result sentinel)]

10 [has-run?

-reset-fn

-(fn [& args]

-(locking sentinel

-(if (= @result sentinel)

15

(reset! result (function))

-@result)))]))

This is the same as the previous create-runonce, with two additions. reset-fn (line 8) simply sets the result back to sentinel, as if the target had never run. The has-run? predicate returns true if the result is not sentinel.

Notice that runonce encapsulates its implementation details. The sentinel and result objects are created in a local let and can never be accessed directly outside the function. Instead, you access them only through their “API,” the three functions returned by runonce. This is similar to

Prepared exclusively for WG Custom Motorcycles

Report erratum

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

WRAPPING UP 210

using a private field in an OO language but is more flexible and more granular.

runonce creates a complete, albeit minimal, dependency system that can work with any Clojure function. The current feature set includes the following:

Run-once semantics for any function

A predicate to check a runonce function’s status

A reset function for testing runonces

All that, and the entire codebase is less than two dozen lines.

Lancet Step 3: runonce

Download lancet/step_3.clj

(defn runonce

"Create a function that will only run once. All other invocations return the first calculated value. The function can have side effects. Returns a [has-run-predicate, reset-fn, once-fn]"

[function]

(let [sentinel (Object.) result (atom sentinel)

reset-fn (fn [] (reset! result sentinel)) has-run? #(not= @result sentinel)]

[has-run? reset-fn (fn [& args]

(locking sentinel

(if (= @result sentinel) (reset! result (function)) @result)))]))

6.8 Wrapping Up

Clojure’s concurrency model is the most innovative part of the language. The combination of software transactional memory, agents, atoms, and dynamic binding that you have seen in this chapter gives Clojure powerful abstractions for all sorts of concurrent systems. It also makes Clojure one of the few languages suited to the coming generation of multicore computer hardware.

But there’s still more. Clojure’s macro implementation is easy to learn and use correctly for common tasks and yet powerful enough for the harder macro-related tasks. In the next chapter, you will see how Clojure is bringing macros to mainstream programming.

Prepared exclusively for WG Custom Motorcycles

Report erratum

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