A few words from Agical

How to mix Clojure and Java code in the same tools-deps project

For whatever reasons, you might sometimes want to develop your program in a mix of Clojure and Java. (Or some other language that compiles to .class files.)

This is actually so easy that nobody has bothered with writing up an article on how to do it. At least not for non-Leiningen projects. Until now, because in processing a pull request on Calva, I had reasons to try create a project like this, and I’d like to help you walk a bit straighter of a path than I did.

TL;DR

Clojure happily imports things from Java .class files using the same import macro as you import any Java things you are using. The files just need to be on the classpath. This means that all you have to do is to make sure your .class files are in a directory reachable from :paths in your deps.edn file. Then you can import away.

(I am super stupid and things that are too easy can take a while for me to figure out. Often because I am overthinking things to compensate for my stupidity.)

Read on for a more complete description.

The project

Since I wasn’t going to actually use the project for something other than development experiments, I just created this directory structure:

├── clojure-java
│   ├── deps.edn
│   └── java
│   │   └── pez
│   │       └── HelloWorld.java
│   └── src
│   │   └── pez
│   |       └── java_hello.clj

The full deps.edn looks like so:

{:paths ["src" "java"]}

In the Java file I am merely exporting (making public) a static String field, a static method and an instance method:

package pez;

public class HelloWorld {
    public static String FOO = "Foo";

    public HelloWorld() {

    }

    public static void main(String[] args) {
        System.out.println("Hello, World!"); 
    }

    public static String foo() {
        return "foo";
    }

    public String bar() {
        return "bar";
    }
}

I compiled this like so:

$ (cd java; javac HelloWorld.java)

Which creates java/HelloWorld.class.

And then in the Clojure code I am importing the class and using a Rich comment block to exercise the ”API”:

(ns pez.java-hello
  (:import [pez HelloWorld]))

(comment
  HelloWorld/FOO
  (HelloWorld/foo)
  (.bar (HelloWorld.))
  )

Starting the project using Calva is just a matter of executing the Jack-in command and selecting deps.edn as the project type. Then I can load the Clojure file and evaluate the top level forms in the Rich comment:

Clojure + Java Evaluations

That’s all there is to it. Told you it is easy! 😀

Pitfalls

Well, a pitfall at least. Figuring this out took me some extra time because I didn’t realize that Clojure will not pick up the latest compiled .class file. Once you have imported it, you are stuck with the definitions in that class file. Since I started with an empty HelloWorld class definition that meant that none of my evals worked.

Once I had gotten some help with suspecting that I might be loading an old definition of my Java code, i didn’t really succeed in finding a nice workflow. The only way i could figure out to get the latest compiled code loaded was to restart the REPL. That feels very alien to someone who has been spoiled with the Clojure way. If you know a more dynamic approach to do this, please let me know. Chat me up on the Clojurian’s Slack, or e-mail me (address below), or comment on this Twitter thread:

Beyond experiments

The simple layout of this project shows with how little ceremony you can set up a mixed JVM language project using Clojure. Though in a real project you will need some more infrastructure.

For creating the project I can recommend deps-new which sets up scaffolding for things like tests, compiling uber-jars, and more. I don’t think it caters for the mixed Java+Clojure scenario (but maybe some project template does?).

To automate the compilation and packaging of your mixed project check this tools.build build script out.

Note: This article is specifically about tools-deps projects. But I should probably at least mention that Leiningen has some explicit support for this scenario. See: Polyglot (Clojure, Java) Projects With Leiningen