EDIT: I didn’t realize when I wrote this that I sounded as critical as I did. I’d like to make clear that this post about a minor philosophical difference and not about any real usability problems with Clojure.
Clojure is probably my current favorite programming language. It combines most of the advantages of Lisp with the most important advantages of Java and introduces a number of ideas not present in either. Some of those ideas nearly unique to Clojure. In all, I believe Clojure occupies a pretty awesome sweet spot in language design that is hard to beat. Despite that, there are at least a few “warts” in the language, (there always are,) and I’m going to write about one here which annoys me.
Clojure’s core language seems to contain a number of leaky macros. For a language community which seems to try so hard to be functional and avoid macros whenever possible, presumably to avoid leaky abstractions, this baffles me. The biggest offender I think, is the “with-open” macro. An example of the with-open macro:
(with-open [output (writer "/foo/bar.txt")]
(cl-format output "printing values out here: ~A and here: ~A"
value1 value2))
This roughly expands to:
(let [output (writer "/foo/bar.txt")]
(try
(cl-format output "printing values out here: ~A and here: ~A"
value1 value2)
(finally (.close output))))
Essentially “with-open” is a simple replacement for “let” which remembers to call “.close” on its only bound variable. Its purpose is to be used to cleanly wrap Java stream handles and close them when they are no longer necessary. Common Lisp as an analogue called “with-open-file”1:
(with-open-file (output "/foo/bar.txt" :direction :output)
(format output "printing values out here: ~A and here: ~A"
value1 value2))
This expands to:
(let ((output (open "/foo/bar.txt" :direction :output)) (#:G969 t))
(unwind-protect
(multiple-value-prog1
(progn
(format output "printing values out here: ~a and here ~a" value1
value2))
(setq #:G969 nil))
(when output (close output :abort #:G969))))
Functionally, this is the same as the Clojure example with one difference, “with-open-file” guarantees the type of the stream object which will be bound. “with-open,” however, does not and allows literally any object to be provided. This means that while “with-open-file” creates, opens, and closes a file handle automatically, “with-open” however, does not. Normally this isn’t much of an issue but it does mean that when a mistake is made the programmer must know how “with-open” works to diagnose it.
This makes “with-open” a leaky macro. “with-open” is supposed abstract out details of working with streams but requires programmers to know that it calls “.close” on a stream object. This is especially egregious because that is literally all “with-open” abstracts out. “with-open” is thus barely an abstraction at all. It just saves some typing.
Clojure is full of these leaky macros. It has “->,” “->>,” and “doto,” to name a few. They are annoying because to use them, one has understand the transformations which they make, and to read or debug code with them in it one has to understand them. Furthermore, because they are macros, not functions, when you misuse them, Clojure’s already useless stack traces will become even less useful.
Now, granted, a lot of these are only there to make dealing with Java objects more palatable but at the same time others are there only make Clojure read more intuitively in some cases and I’m not totally positive that it is worth the potential confusion.
Anyway, instead of “with-macro” how about I suggest and alternative. The reason “with-macro” is written the way it is is to make it a general as possible so it can be used with any Java stream object. A simple fix would be to add a clause to check that whatever object was passed in implemented the Java streams interface:
(if-not (isa? (type ~object) java.io.Closable)
(throw (Exception. (str "Object: " ~object " cannot be closed, (is not a stream)"))))
This will at least solve the debugging issue. Alternatively one could have “with-open” create, open, and close the object in the same vein as “with-open-file.” An argument could be provided to determine whether to create a BufferedReader, BufferedWriter, or other applicable Java object. If the argument method is too potentially limiting, one could instead use a macro-writing-macro to create separate “with-open-bufferedreader,” “with-open-bufferedwriter” macros and any new macros future programmers might think useful:
(defmacro define-with-open-macro [type]
`(defmacro ~(symbol (str "with-open-" (name ~type))) [[binding & args] & body]
`(let [~binding (new ~type ~@args)]
(try
~@body
(finally (. ~binding close))))))