Leaky Clojure Macros

EDIT: I did­n’t real­ize when I wrote this that I sounded as crit­i­cal as I did. I’d like to make clear that this post about a minor philo­soph­i­cal dif­fer­ence and not about any real usabil­ity prob­lems with Clo­jure.

Clo­jure is prob­a­bly my cur­rent favorite pro­gram­ming lan­guage. It com­bines most of the advan­tages of Lisp with the most impor­tant advan­tages of Java and intro­duces a num­ber of ideas not present in either. Some of those ideas nearly unique to Clo­jure. In all, I believe Clo­jure occu­pies a pretty awe­some sweet spot in lan­guage design that is hard to beat. Despite that, there are at least a few “warts” in the lan­guage, (there always are,) and I’m going to write about one here which annoys me.

Clo­jure’s core lan­guage seems to con­tain a num­ber of leaky macros. For a lan­guage com­mu­nity which seems to try so hard to be func­tional and avoid macros when­ever pos­si­ble, pre­sum­ably to avoid leaky abstrac­tions, this baf­fles me. The biggest offender I think, is the “with­-open” macro. An exam­ple 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")]
    (cl-format output "printing values out here: ~A and here: ~A"
               value1 value2)
    (finally (.close output))))

Essen­tially “with­-open” is a sim­ple replace­ment for “let” which remem­bers to call “.close” on its only bound vari­able. Its pur­pose is to be used to cleanly wrap Java stream han­dles and close them when they are no longer nec­es­sary. Com­mon Lisp as an ana­logue 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))
           (format output "printing values out here: ~a and here ~a" value1
        (setq #:G969 nil))
    (when output (close output :abort #:G969))))

Func­tion­al­ly, this is the same as the Clo­jure exam­ple with one dif­fer­ence, “with­-open-­file” guar­an­tees the type of the stream object which will be bound. “with­-open,” how­ev­er, does not and allows lit­er­ally any object to be pro­vid­ed. This means that while “with­-open-­file” cre­ates, opens, and closes a file han­dle auto­mat­i­cal­ly, “with­-open” how­ev­er, does not. Nor­mally this isn’t much of an issue but it does mean that when a mis­take is made the pro­gram­mer must know how “with­-open” works to diag­nose it.

This makes “with­-open” a leaky macro. “with­-open” is sup­posed abstract out details of work­ing with streams but requires pro­gram­mers to know that it calls “.close” on a stream object. This is espe­cially egre­gious because that is lit­er­ally all “with­-open” abstracts out. “with­-open” is thus barely an abstrac­tion at all. It just saves some typ­ing.

Clo­jure is full of these leaky macros. It has “->,” “->>,” and “do­to,” to name a few. They are annoy­ing because to use them, one has under­stand the trans­for­ma­tions which they make, and to read or debug code with them in it one has to under­stand them. Fur­ther­more, because they are macros, not func­tions, when you mis­use them, Clo­jure’s already use­less stack traces will become even less use­ful.

Now, grant­ed, a lot of these are only there to make deal­ing with Java objects more palat­able but at the same time oth­ers are there only make Clo­jure read more intu­itively in some cases and I’m not totally pos­i­tive that it is worth the poten­tial con­fu­sion.

Any­way, instead of “with­-­macro” how about I sug­gest and alter­na­tive. The rea­son “with­-­macro” is writ­ten the way it is is to make it a gen­eral as pos­si­ble so it can be used with any Java stream object. A sim­ple fix would be to add a clause to check that what­ever object was passed in imple­mented 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 debug­ging issue. Alter­na­tively one could have “with­-open” cre­ate, open, and close the object in the same vein as “with­-open-­file.” An argu­ment could be pro­vided to deter­mine whether to cre­ate a Buffere­dRead­er, Buffered­Writer, or other applica­ble Java object. If the argu­ment method is too poten­tially lim­it­ing, one could instead use a macro-writ­ing-­macro to cre­ate sep­a­rate “with­-open-buffere­dread­er,” “with­-open-buffered­writer” macros and any new macros future pro­gram­mers might think useful:

(defmacro define-with-open-macro [type]
  `(defmacro ~(symbol (str "with-open-" (name ~type))) [[binding & args] & body]
     `(let [~binding (new ~type ~@args)]
          (finally (. ~binding close))))))


  1. Technically “with-open-stream” is the closer analogue, but it is less often used. “with-open-file” expands to “with-open-stream.” 
  2. I’ve not tested this macro. 

Last update: 29/02/2012

blog comments powered by Disqus