Search This Blog

Programming Perils

The collected random musings of some guy who writes software.

Friday, May 28, 2010

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

Over the years, I've really come to love Maven. Funny thing is, I didn't care for Maven much at first. It took managing a medium sized project using ANT, then having to scale up to a much larger set of projects, before I could appreciate what Maven had to offer. It's not just a build technology, it's a software project management methodology where Maven is only one component. If you are one of those people who don't "get" Maven, that's okay. I used to be in that camp too, back in simpler times.

At this point in my career, I am firmly committed to Maven. Occasionally, though, It'd be really nice to break out of the whole pre-packaged plugin model, write some script, maybe call an ANT task, and just get the job done.

Lucky for me, there's a plugin out there that lets me do just that. The Sandflea maven-clojure-plugin lets me execute Clojure scripts as part of a Maven build, with complete access to the Maven API.

Remember the (ant ...) macro from part 1, and how we used it with the ZIP ANT task? Well, here's an example of using it in a Maven POM to create a ZIP artifact. Yes, this example is a bit contrived, but it's easy enough to understand.
<plugin>
  <groupId>sandflea.clojure</groupId>
  <artifactId>maven-clojure-plugin</artifactId>
  <executions>
    <execution>
      <id>Create ZIP</id>
      <goals><goal>execute</goal></goals>
      <phase>package</phase>
      <args>
        <source-dir>${project.basedir}/src/main/assembly</source-dir>
        <target-zip>${project.build.directory}/${project.artifactId}.zip</target-zip>
      </args>
      <configuration>
        <source>
        (ns my-project.create-zip
          (:import
            [java.io File]
            [org.apache.tools.ant.taskdefs Zip])
          (:use [sandflea.clojure.buildutil]))
        (ant (Zip.)  
          (.setBasedir  (File.(*args* "source-dir")))
          (.setDestFile (File.(*args* "target-zip")))
          (.compress true) )
        </source>
      </configuration>
    </execution>
  </executions>
</plugin>

I get to have my cake and eat it too! I know I can rely on steady and predictable Maven to always be there for me, and whenever I need to spice things up a bit, ANT is just a (function) call away. Isn't life sweet?

Thursday, May 27, 2010

Overheard at a team building event...

"The company that sweats together ... stinks together."

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!

Tuesday, May 25, 2010

The Stone Mind

Hogen, a Chinese teacher, lived alone in a small temple in the country. One day three traveling monks appeared and asked if they might make a fire in his yard to warm themselves.

While they were building the fire, Hogen heard them arguing about subjectivity and objectivity. He joined them and said, "There is a big stone.  Do you consider it to be inside or outside your mind?"

One of the monks replied, "From the Buddhist viewpoint, everything is an objectification of mind, so I would say that the stone is inside my mind."

"Your head must feel very heavy," observed Hogen, "if you are carrying around a stone like that in your mind."

Upon hearing this rebuke, the second monk replied, "Then I would say the stone is outside my mind."

"There is no stone," observed Hogen.

The third monk, silent up to this point, said, "Using Blender I have created a sculpted replica of the stone which is indistinguishable from the original.  This I have uploaded into Second Life. Everyone there believes it is a very good stone.  The stone is inside my computer."

"Mu," replied Hogen.

Followers