Search This Blog

The collected random musings of some guy who writes software.

Wednesday, May 26, 2010

Part 1: Clojure, my relationship with Maven, and a sordid affair with ANT

I outgrew ANT a long time ago. The one-off nature of every ANT script, coupled with the lack of dependency management, drove me into the arms of Maven. Still, I missed some of the things that ANT let me do almost effortlessly, like copying entire folders and creating ZIP files. Don't get me wrong, Maven is great ... but she can be kind of rigid, if you know what I mean. So every once in a while, ANT and I get back together to rekindle the old flame.

Let me be perfectly clear. I'm only interested in one thing. ANT is a complete build system. It includes an XML scripting language along with methodologies for extensibility, build targets, etc. None of that stuff peaks my interest. I just want the tasks. ANT tasks let me do all sorts of things that are otherwise difficult to do in Java. I mean, just take a look at Overview of ANT Tasks. All that general-purpose, prepackaged functionality is right there in your face, begging, "Take me! Take me now!"

But this isn't just about ANT. It's about Clojure too.

Consider the lowly "ZIP" ANT task. If you've been around Java for any period of time, you've ZIPped files using an ANT script. Without much effort, you can call this task (or any other ANT task) directly from Java. Here's an example:

Zip zip = new Zip();
zip.init();
zip.setProject(new Project());
zip.setBasedir(sourceFolder.getCanonicalFile());
zip.setDestFile(targetZip.getCanonicalFile());
zip.setCompress(true);
zip.execute();

To a Java developer, this seems like perfectly reasonable code. Nevermind that calls to .init(), .setProject(), and .execute() are purely boilerplate. This program gets the job done, and with significantly less code than if you wrote this longhand using ZipFile or ZipOutputStream. And it's a whole lot better than having to generate an XML file and launch ANT as an external process to run it.

The Java code in this example is still far from ideal. It requires roughly twice as many lines as it should along with redundant syntax. But I am getting ahead of myself. Let me explain.

The XML script for an ANT build is a type of DSL (Domain-Specific Language). Groovy provides another DSL for working with ANT, and the equivalent code looks like this:

ant.zip(
  basedir: sourceFolder.canonicalFile, 
  destfile: targetZip.canonicalFile, 
  compress: true)

Oh, didn't I mention that Groovy was somehow involved in this? Yet another old flame...

Notice that this code doesn't require any of the boilerplate from the Java example. It contains only relevant, meaningful information. I want to use "ant" to "zip" the "source-folder" into a "target-zip" file. "Compress" it. It reads almost like English.

The Groovy code does require a DSL which, while not trivial to write, isn't even possible in Java. And this DSL makes heavy use of dynamic-dispatch which can affect performance (though not so much in this instance). All that is somewhat beside the point - the code is beautiful and elegant.

Recently, I've been eyeing Clojure, an updated version of Lisp for the JVM. Okay, hear me out on this one. I started this as a thought-experiment, something to stretch my brain. I thought it would be a passing fancy, really. I've since come to realize that Clojure is practical, providing tidy solutions to certain problems that are considered "hard" or "impossible" in Java. And don't forget the most important part - it's kind of ... exciting!

So, let's look at rewriting our ZIP task example in Clojure. I could use Stuart Halloway's Lancet, but I really want to see how difficult it would be to solve this using Clojure's core toolset.

To get started, I'll translate the original Java example directly into Clojure syntax. For a quick syntax primer, check out Clojure - Java Interop. Notice that in Clojure, the method (function) comes first, the object second, and any arguments follow. Surprisingly, the number of parens is about the same as in the Java example, and there are no semi-colons. I think this is already quite readable, but it's still not as clean as the Groovy example.

Clojure #1
(let [zip (Zip.)]
  (.init zip)
  (.setProject zip (Project.))
  (.setBaseDir zip (.getCanonicalFile source-folder))
  (.setDestFile zip (.getCanonicalFile target-zip))
  (.setCompress zip true)
  (.execute zip))

Lucky for me Clojure provides the (doto ...) macro to help clean up code like this. In the previous example, we've had to include zip in every function call. That's redundant. Using (doto ...), we don't have to define a variable - we can just operate directly on the new instance of Zip.

Clojure #2
(doto (Zip.)
  (.init)
  (.setProject (Project.))
  (.setBaseDir (.getCanonicalFile source-folder))
  (.setDestFile (.getCanonicalFile target-zip))
  (.setCompress true)
  (.execute))

(doto ...) alters the syntax of function calls, locally, so we don't have to write zip over and over. What's impressive about this is that (doto ...) is not a special form of the language. It's a macro. If you run (macroexpand-1 ...) on example 2, you get something that looks a lot like example 1. If you wanted to do the same sort of thing in Java, you'd probably implement source preprocessing with ANTLR along with some complex EBNF grammars. Check out the documentation and source for (doto ...) to get an idea of how incredibly simple it is to modify the syntax of Clojure using macros. Read about homoiconicity to get an idea of why this is possible in Clojure. And it's not what you're probably thinking.

The second Clojure example cleans up the code a bit, but it still includes the boilerplate. After a bit of experimentation (the Clojure REPL encourages experimentation), I came up with this macro. This macro reuses (doto ...) and includes the boilerplate function calls I need to run an ANT task.

(defmacro ant [task & ops]
  (concat 
    (concat 
      (list 
        'doto task 
        '(.init) 
        '(.setProject(org.apache.tools.ant.Project.))) 
      ops) 
    '((.execute)) ))

A macro defines Clojure code that replaces the source code at compile time. With this macro, I can call the ANT ZIP task from Clojure like this:

Clojure #3
(ant (Zip.)
  (.setBaseDir (.getCanonicalFile source-folder))
  (.setDestFile (.getCanonicalFile target-zip))
  (.setCompress true))

Wow! This is really close to the syntax of the Groovy DSL for ANT. There is no redundancy, no extraneous information, and no boilerplate code. It reads almost like English. I was able to write a simple DSL for ANT tasks in about 5 minutes with a simple Clojure macro.

Another big advantage of using macros is performance. Macros aren't like function wrappers. They actually rewrite code, inline, as you compile it. Clojure examples #1, #2, and #3 will all run with the same speed. There are no function wrappers or dynamic-dispatch mechanisms to slow it down.

I think I'm falling in love all over again!

No comments:

Post a Comment

Followers