Advanced Clojure/Java Mixing in Leiningen
posted
Assume you have come to the conclusion that you need some Java code in your Clojure project. It’s either for readability (!) or maybe because it’s more performant. Regardless of the reason, you want it to be easy to work with in Clojure, and that requires some of the Clojure records you’ve defined. However, as the remaining part of the code depends on the actual Java code, you need to compile parts of the Clojure code, then Java, then the remaining Clojure code. I’ll explain how you can do exactly that in Leiningen, by showing the start of a Java lexer which emits Clojure records and is easy to integrate with idiomatic Clojure.
To save yourself from copypasting the example, you can clone this GitHub project to try and play with the sample project.
Step 1: Define the Record
The first step is obviously to define the namespace in which the record is
defined. This is done in exactly the same way as you’d do without Java code.
This code should lie inside src/clj/myproject/records.clj
, if your Leiningen
:source-paths
contains the string "src/clj"
.
(ns myproject.records
(:import (clojure.lang Keyword)))
(defrecord Item [^Keyword type ^String value])
Here, we say that the record is named Item, and that it contains a type (keyword) and a value (string). You can, of course, extend this record with methods and types later on, or add in more records if you want to. I’ve additionally added in type hinting, but this is of course not necessary.
Of course, this also works with :gen-class
‘ed Clojure files, if you need such
functionality.
Step 2: “Implement” the Lexer
The next step is to implement the desired functionality in the lexer. In our case, we’ll not implement anything, except for a check to see that the state is contained and that the compilation actually works.
To do this, we’ll use the fact that iterator-seq
returns a seq over a Java
Iterable class, and just call iterator-seq
on the Java Lexer. This will make
the result “immutable”, and as an additional bonus, iterator-seq
will also
make the seq thread safe. Instead of throwing exceptions, we’ll instead have an
error item which contains enough information to do something sensible.
The long is, of course, a placeholder, and is just used to simulate state.
So here’s the code. If you have :java-source-paths ["src/java"]
, then the file
should be named src/java/myproject/Lexer.java
and should contain the
following:
package myproject;
import java.util.Iterator;
import myproject.records.Item;
// This is our Item record
import clojure.lang.Keyword;
// We want to use keywords
public class Lexer implements Iterator<Item> {
public static final Keyword ERROR =
Keyword.intern(null, "error");
private long count;
public Lexer(long count) {
this.count = count;
}
public void remove() {
throw new UnsupportedOperationException();
}
public Item next() {
if (0 < count) {
count--;
return new Item(ERROR, String.format("%d", count));
}
}
public boolean hasNext() {
return 0 < count;
}
}
Please note that this is NOT idiomatic Java code and that you should carefully think about how the Java code should look like. The intent is to make it easier to integrate with Clojure, but consider the design before doing things like this.
Step 3: Use the Lexer
This step is easy. We just create a lexer, call iterator-seq
with it, and print
the result with clojure.pprint/pprint
. Assuming same :source-paths
as
earlier, this file should be named src/clj/myproject/main.clj
.
(ns myproject.main
(:require [clojure.pprint :as pp]])
(:import myproject.Lexer)
(:gen-class))
(defn -main [& args]
(pp/pprint (iterator-seq (Lexer. 5))))
And you can, of course, use Java method calling, type hinting, and everything else which comes with normal interop. There’s nothing different from the Java code dependent on Clojure code compared to normal Java code.
Step 4: The Magic Glue
The magic trick to get this working properly is Leiningen’s :prep-tasks
option. :prep-tasks
is a vector of tasks needed to run before
project-dependent commands can be run. They are guaranteed to run before those
tasks, in the order specified.
The default value is ["javac" "compile"]
, which compiles ALL java files,
then compiles all the specified :aot
files. We want that the
myproject.records
namespace is compiled before we perform javac, so that the
myproject.records.Item
record is available for the java lexer. Both compile
and javac can take input arguments to specify specific files to compile. In this
example, it suffices to just call compile with "myproject.records"
to compile
the namespace, but it is also possible to use regexes to specify namespaces. For
information on how to use regexes, have a look at issue #1442 as
it’s not completely straightforward yet.
However, let’s not dwell too much on the details. To do the record compilation
first, we just add ["compile" "myproject.records"]
in front of the default
preparation steps. Here’s how the project.clj
looks like if we do that:
(defproject myproject "0.1.0-SNAPSHOT"
:description "Example on clj->java->clj compilation"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:source-paths ["src/clj"]
:java-source-paths ["src/java"]
:prep-tasks [["compile" "myproject.records"]
"javac" "compile"]
:dependencies [[org.clojure/clojure "1.5.1"]]
:profiles {:uberjar {:aot :all}}
:main myproject.main)
So, what’s the output? lein do clean, run
returns the following result:
;; Compiling myproject.records
;; Compiling 1 source files to ~/jeannikl/myproject/target/classes
({:type :error, :value "4"}
{:type :error, :value "3"}
{:type :error, :value "2"}
{:type :error, :value "1"}
{:type :error, :value "0"})
Success! Note that pprint
hides record names. If you replace it with prn
,
you’ll see the actual record type.
With :prep-tasks
, you can have many more steps if you need to. You hopefully
have an understanding of what’s going on with :prep-task
, and how you can use
the compile
and javac
task to compile different internal dependencies in the
correct order, no matter how complicated! (Whether having a complicated build is
sensible or not is an entirely different matter.)