hyPiRion

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.)

And again, this example is available as a github project.