In the last post, we took a look at basic usage of Clojure's agent function. In this post, we'll dive a little bit deeper
Validation
We glossed over the options that you can define when creating an agent; one of them is the validator which one can use to check before the agent is updated with the passed value.
If we want to make sure that our read-agent always gets a string value, this is all we have to do:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(def read-agent (agent "" :validator string?)) |
Similarly, any function that takes a single value as a parameter can be used here. As you can see, we had to change our default value for the agent from nil to "" since there is now a string validator. If we hadn't, any time we tried to use that agent, we'd get java.lang.IllegalStateException.
When Things Go Wrong
Another option you can set when defining an agent is the error handler. This will be used in the event of an error, including if a value fails to be validated by your validator function. Here's an example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn read-agent-error-handler [agnt, exception] | |
(println "Whoops! " agnt " had a problem: " exception)) | |
(def read-agent (agent "" :error-handler read-agent-error-handler)) |
With both of these options, you don't have to set them when the agent is defined; you can do it later with a function call, if you so desire (or if needs demand it):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(set-validator! read-agent string?) | |
(set-error-handler! read-agent read-agent-error-handler) |
Watch This!
So, we've got an error handler but no event handler? Yup. However, you can actually get callback-like behavior using watches. Check this out:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn read-watch [key agnt old-value new-value] | |
(println "File has been read!") | |
(println (str "Watch key: " key)) | |
(println (str "Agent: " agnt)) | |
(println (str "Agent's old value: " old-value)) | |
(println (str "Agent's new value: " new-value))) | |
(add-watch read-agent "reader-01" read-watch) |
Now, any time our agent's state changes, the function passed to the watch will fire. As described in the docs, the parameters are: the agent, a key of your devising (must be unique per agent), and the handler that you want to have fired upon state change. The handler takes as parameters: the key you defined, the agent, the agent's old value, and the agent's new value.
All Together Now
With all our example code in place, we can now exercise the whole thing at once. Here's the whole thing:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn read-agent-error-handler [agnt, exception] | |
(println "Whoops! " agnt " had a problem: " exception)) | |
(def read-agent | |
(agent | |
"zero bytes" | |
:validator string? | |
:error-handler read-agent-error-handler)) | |
(defn big-read [old-value seconds] | |
"Pretent to read a really big file" | |
(time (Thread/sleep (* seconds 1000))) | |
"<contents of big file>") | |
(defn read-watch [key agnt old-value new-value] | |
(println "File has been read!") | |
(println (str "New file data is: " new-value)) | |
(println "")) | |
(add-watch read-agent "reader-01" read-watch) |
To simply demonstrate the async nature and the callbacks in action, let's run the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ start-clojure | |
Clojure 1.4.0 | |
user=> (ns async-2) | |
nil | |
async-2=> (load-file "05-agent.clj") | |
#<Agent@2598c6f3: "zero bytes"> | |
async-2=> | |
async-2=> (send-off read-agent big-read 20) | |
#<Agent@2598c6f3: "zero bytes"> | |
async-2=> | |
async-2=> ; let's do something else while we're waiting | |
async-2=> ; maybe some simple math? | |
async-2=> (+ 1 1) | |
2 | |
async-2=> |
Eventually, our callback will render output very much like the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async-2=> "Elapsed time: 10000.776 msecs" | |
File has been read! | |
New file data is: <contents of big file> |
Do note, however, that if we called a series of send-offs with different times (using the same agent and watch), we wouldn't see the ones with shorter times come back first. We'd see the callback output in the same order we called send-off. This is because the watch function is called synchronously on the agent's thread before any pending send-offs (or sends) are called. In future posts, I'll cover ways around this (constructing agents on the fly as well as exploring alternative solutions with external libraries).
Regardless, with these primitives, there are all sorts of things one can do. For instance...
Dessert
To close, check out this neat little bit of code that sends 1,000,000 messages in a ring. This code creates a chain of agents, and then actions are relayed through it (taken from the agents doc page):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defn relay [x i] | |
(when (:next x) | |
(send (:next x) relay i)) | |
(when (and (zero? i) (:report-queue x)) | |
(.put (:report-queue x) i)) | |
x) | |
(defn run [action-count agent-count] | |
(let [q (new java.util.concurrent.SynchronousQueue) | |
hd (reduce (fn [next _] (agent {:next next})) | |
(agent {:report-queue q}) | |
(range (dec action-count)))] | |
(doseq [i (reverse (range agent-count))] | |
(send hd relay i)) | |
(.take q))) |
Kicking this puppy off, our million messages finish in about 1 second :-)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async-2=> (time (run 1000 1000)) | |
"Elapsed time: 1003.892 msecs" | |
0 |