Cleaning Up Resources

In many languages, including Java, safely cleaning up resources is a bit tricky if you haven’t done it before. Take reading from a file as an example: first we need to open the file resource, then do our work with it, and then when we’re finished, we need to close the resource. It’s just a three-step process, but there’s a lot that can go wrong. There might be an exception when reading from the file. There might have been an exception when opening the file. For this reason, it’s not uncommon to see Java like the following code sample to read a file’s contents into a string:

context/teardown_filestream.java
 
public​ ​String​ readFile(​String​ filePath) ​throws​ IOException {
 
FileInputStream​ fileStream = null;
 
StringBuilder​ contents = ​new​ ​StringBuilder​();
 
byte​​[]​ buffer = ​new​ ​byte​[4096];
 
try​ {
 
fileStream = ​new​ ​FileInputStream​(filePath);
 
while​ (fileStream.read(buffer) != -1) {
 
contents.append(​new​ ​String​(buffer));
 
}
 
} ​finally​ {
 
if​ (fileStream != null) {
 
fileStream.close();
 
}
 
}
 
return​ contents.toString();
 
}

Let’s ignore for the purposes of this discussion the fact that reading a file into a string is an engineering mistake (because what if we’re given a gigantic file?). Let’s think about string appending as a special case of a general problem where we have a resource we need to clean up. Now, granted, Java 7 has finally (zing!) introduced a language feature to make this cleaner: try-with-resources.[15] But users of many other language (and Java users stuck on Java 6 or below) aren’t so lucky. The core algorithm here seems particularly obscured by this try/finally pattern and the separate fileStream declaration to work around the block scoping. With macros to allow contextual evaluation, one nice solution is built into a macro, with-open:

context/with_open.clj
 
(​defmacro​ ​with-open
 
"bindings => [name init ...]
 
 
Evaluates body in a try expression with names bound to the values
 
of the inits, and a finally clause that calls (.close name) on each
 
name in reverse order."
 
{:added ​"1.0"​}
 
[bindings & body]
 
(assert-args
 
(​vector?​ bindings) ​"a vector for its binding"
 
(​even?​ (​count​ bindings)) ​"an even number of forms in binding vector"​)
 
 
(​cond
 
(​=​ (​count​ bindings) 0) `(​do​ ~@body)
 
(​symbol?​ (bindings 0)) `(​let​ ~(​subvec​ bindings 0 2)
 
(​try
 
(​with-open​ ~(​subvec​ bindings 2) ~@body)
 
(finally
 
(​.​ ~(bindings 0) close))))
 
:else (​throw​ (IllegalArgumentException.
 
"with-open only allows Symbols in bindings"​))))
context/with_open_client.clj
 
(​import​ 'java.io.FileInputStream)
 
(​defn​ read-file [file-path]
 
(​let​ [buffer (​byte-array​ 4096)
 
contents (StringBuilder.)]
 
(​with-open​ [file-stream (FileInputStream. file-path)]
 
(​while​ (​not​​=​ -1 (​.​​read​ file-stream buffer))
 
(​.​append contents (String. buffer))))
 
(​str​ contents)))

You’ve seen with-open in use a few times already, as part of the implementation of with-out-file and with-in-str. Its definition is a bit more involved than some of the macros you’ve seen, but it’s nothing scary. It checks a few assumptions up front with assert-args, throwing an exception if the binding expression is not a symbol, and behaves recursively in case there are multiple resources to be cleaned up. Recursive macro definitions like this one can be very useful, though as with any other recursive algorithm it’s important to ensure that the recursion will eventually terminate. Typically we want each recursive call to have something of a smaller value, moving toward the base case.

Notice that the details of the try/finally, along with the null check, have been abstracted away into the with-open macro. Otherwise, this is a pretty faithful translation of the Java, but all the ceremony around cleaning up the stream has just gone away. And it’s not hard to imagine a version of with-open that would also wrap a try/catch around the .close call, in order to swallow or otherwise specially handle an exception when trying to close the resource.

Abstracting Away the Details

The main point here, and in the other examples in this chapter, is that macros allow you to eliminate the noisy details of cleaning up an open resource, or rescuing errors, or setting up dynamic bindings or other contexts for evaluation. By writing macros to abstract away these contextual details, you can clarify the core operations you’re performing to make your code’s purpose more obvious to the people who will read it in the future (including yourself!). This is a core ability of macros, and you’ll see lots of overlap between this category and ones we’ll cover later.

Now that you’ve seen some of the ways you can use macros to abstract away the context in which your code will evaluate, we’ll look at ways to use both abstraction and more macro-specific tools to improve the runtime performance of our Clojure code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.180.43