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
-
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. ↩ -
This isn't really fair, of course. If you're running
lein v 2.1
or later, we could've runlein new app hello
instead oflein new hello
. This would've generated a runnable 'hello world' application including themain
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. ↩