ben vandgrift · on living the dream

Clojure: 'Hello World' from the Command Line

As it's commonly presented, Clojure isn't really intended to produce command-line applications. As a jvm language, Clojure is for Big Things, Big Ideas, Hard Problems.

Leiningen is the dominant build tool for Clojure, and it's not really close. These two things in combination yield a strange landscape for someone looking to create a simple command line app in Clojure. The 'Hello World' you might find in other language is missing.

If that hasn't inspired you to turn to a life of crime, let's walk through what it takes to run something in Clojure and lein from the command line.1

If you don't have your environment set up, you should do that first. Otherwise, let's start a project 'hello'.

$ lein new hello

If this is the first time you've run lein, you'll see it download a list of dependencies. You will get used to this in time-it's not really all that different from the way rubygems works, save for the part where you need eleven .pom and eight .jar files to write a 'hello' command-line script.

Now that's over with,lein has created a few files in your hello project that are worth looking at:

./project.clj
./src
./src/hello
./src/hello/core.clj

project.clj is the project's setup file, and src/hello/core.clj contains the code we'll be executing to say 'hello'. Let's have a look at src/hello/core.clj:

(ns hello.core)
(defn foo
  "I don't do a whole lot."
  [x]
  (println x "Hello, World!"))

Sweet! Looks like without doing anything really, we have a working 'hello' application. Let's run it.

$ lein run
No :main namespace specified in project.clj.

What happened here?2 lein doesn't know which namespace holds the project's main function. Which isn't surprising, because by default, the main method isn't generated for us. Let's tell it by adding:main hello.core to the project file:

(defproject hello "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.4.0"]]
  :main hello.core)

Next we change the definition of foo to -main. The '-' in front of -main indicates that the function is static, obviously.

(ns hello.core)
(defn -main
  "This should be pretty simple."
  []
  (println "Hello, World!"))

Problems solved, let's run it:

$ lein run
Compiling hello.core
Hello, World!

Excellent. We have everything we need to run our program with java, right? Nope. There's a few more steps if you want to be able to take a .jar file and move it around with impunity.

First, understand that Clojure was designed to be operated from the repl, not from the command line. As such, it doesn't by default generate any classes that can be packaged for shipping. If you were to lein compile, you'd see that we've built the following files:

./target
./target/classes
./target/classes/hello
./target/classes/hello/core$_main.class
./target/classes/hello/core$loading__4784__auto__.class
./target/classes/hello/core__init.class

What's missing? classes/hello/core.class. We can instruct the clojure compilation process to create the class files by adding a directive to our namespace declaration:

(ns hello.core
  (:gen-class)) ;; see doc for more options

Now we can package a freshly generated class into a .jar file, however we still won't be able to run our application with java unless we have the clojure-1.4.0.jar and all its various dependencies are either on the system's classpath or passed in on the command line. This is a problem endemic to all jvm languages, and not just Clojure. That might look something like this:

$ java -jar target/hello-0.1.0-SNAPSHOT.jar # OR
$ java -cp [jars] -jar target/hello-0.1.0-SNAPSHOT.jar

We're still not very portable. To achieve real portability and the ability to run our application from the command line, we'll need to use lein to create a -standalone jar:

$ lein uberjar
Compiling hello.core
Created ./target/hello-0.1.0-SNAPSHOT.jar
Including hello-0.1.0-SNAPSHOT.jar
Including clojure-1.4.0.jar
Created ./target/hello-0.1.0-SNAPSHOT-standalone.jar

Success! Let's run it.

$ java -jar target/hello-0.1.0-SNAPSHOT-standalone.jar 
Hello, World!

"Simple made easy," am I right?

Hopefully, we've demonstrated two things: first, that using clojure with lein to begin, compile, and run applications is not straightforward, and may be counter-intuitive for someone coming from a language like Ruby.

Second, Clojure is not really designed for command line operation, or any number of simple and straightforward tasks. In this respect it takes what would in many other modern languages the simplest of endeavors and adds multiple layers of complication on top of it.

A true Clojure enthusiast would point out that these types of tasks are beneath their notice. That Clojure is used for more Important Things, and that given the scale of application wherein Clojure really shines, this overhead is negligible.

I'd agree with that sentiment. As a developer, we should have many tools in our belt—solutions for every scale. Clojure is fantastic at scale. It is not good at simple tasks. Leiningen is very useful for projects above a certain size, and otherwise creates some overhead.

In any case, let's not do this again soon.

Footnotes

  1. All snark aside, running a single .clj file (say, hello.clj) as an app without lein looks like this: java -server -cp $CLASSPATH clojure.main hello.clj This is an easy alias in a .bash_profile or what have you.

  2. This isn't really fair, of course. If you're running lein v 2.1 or later, we could've run lein new app hello instead of lein new hello. This would've generated a runnable 'hello world' application including the main function as well as the :gen-class directive on the namespace. The default template is a library, not an application. The rigmarole remains an effective exercise.

written: Mar 13 2013