Java Calling Clojure
Java calling Clojure.
Posted by Uncle Bob on Friday, August 07, 2009
While I think Clojure is an interesting language, in order for it to be of real practical use, I must be able to use it in conjunction with other systems I am working on. Specifically, I’d like to write some FitNesse tools in Clojure; but for that to work, I’ll need to call into my Clojure code from Java.
Today, my son Justin and I managed to do just that by following the instruction in Stuart Halloway’s book, the Clojure api website, and Mark Volkmann’s very usefulsite.
Be advised, that it takes a bit of fiddling to get this to work. You will have to jockey around with your classpaths and output directories. But it’s not actually that hard to do, and the results can be very rewarding.
We implemented the Bowling game (again), but this time we wrote the unit tests in Java, and had them call into Clojure. From the point of view of the Java tests, it looked just like we were calling into java code. The tests had no idea that this was Clojure.
Here are the tests:
package bowling; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; public class BowlingTest { private Game g; @Before public void setup() { g = new Game(); } @Test public void roll() throws Exception { g.roll(0); } @Test public void gutterGame() throws Exception { rollMany(20, 0); assertEquals(0, g.score()); } private void rollMany(int n, int pins) { for (int i=0; i< n; i++) { g.roll(pins); } } @Test public void allOnes() throws Exception { rollMany(20, 1); assertEquals(20, g.score()); } @Test public void oneSpare() throws Exception { g.roll(5); g.roll(5); g.roll(3); rollMany(17, 0); assertEquals(16, g.score()); } @Test public void oneStrike() throws Exception { g.roll(10); g.roll(5); rollMany(17,0); assertEquals(20, g.score()); } @Test public void perfectGame() throws Exception { rollMany(12, 10); assertEquals(300, g.score()); } @Test public void allSpares() throws Exception { rollMany(21, 5); assertEquals(150, g.score()); } }
The clojure code to make them pass looks like this:
(ns bowling.Game (:gen-class :state state :init init :methods [ [roll [int] void] [score [] int] ])) (defn -init [] [[] (atom [])]) (defn -roll [this pins] (reset! (.state this) (conj @(.state this) pins))) (declare score-frames) (defn -score [this] (reduce + (take 10 (score-frames [] (conj @(.state this) 0))))) (defn score-frames [scores [first second third :as rolls]] (cond (or (empty? rolls) (nil? second) (nil? third)) scores (= 10 first) (recur (conj scores (+ 10 second third)) (next rolls)) (= 10 (+ first second)) (recur (conj scores (+ 10 third)) (nnext rolls)) :else (recur (conj scores (+ first second)) (nnext rolls))))
The magic is all up in that (ns bowling.game… block.
The name of the java class is Game in the bowling package, or just bowling.Game.
Clojure will create a special member function named state that you can use to squirrel away the state variables of the class.
A function named -init will be called when your class is constructed. You must write this function to return a vector containing 1) a vector of all the arguments to pass to the base class constructor (in my case none), and the initial state of the newly created instance, (in my case an empty vector stuffed into an atom)
The class will have two new methods, roll, which takes an int and returns void, and score which takes nothing and returns an int.
When a method is called on the instance, Clojure automatically invokes a function of the same name, but prefixed by a -, so -roll and -score are the implementations of the roll and score methods. Note that each of these functions take a thisargument. You can get the state out of this by saying (.state this).
From a source code point of view, that’s about all there is to it. But how do you compile this into a java .class file?
Here’s what we did. We created a new file named compile.clj that looks like this:
(set! *compile-path* "../../classes") (compile 'bowling.Game)
We can run this file from our IDE (IntelliJ using the LAClojure plugin) and it will compile quite nicely. But there are a few things you have to make sure you do.
Find out where your IDE puts the .class files, and set the compile-pathvariable to that directory.
Also make sure that directory is in your classpath.
Make sure that directory exists!
Also make sure that your source file directory (the directory that contains the packages) is in your classpath. (I know… but that’s apparently what it takes.)
You will get errors. And the errors aren’t particularly informative. The key to understanding them is to look at the backtrace of the exceptions they throw. Notice, for example, if the function “WriteClassFile” (or something like that) is buried in the backtrace. If so, you are probably having trouble writing your class file.
In the end, we wrote up an ant script that compiled our clojure and our java together. (You have to compile them in the right order! If java calls Clojure, those .class files have to exist before the java will compile!)
The ant script we used was created for us by IntelliJ, and then we modified it to get it to work. I include it here with the caveat that we made it work, but we didn’t make it “right”. You can ignore most of the stuff at the beginning. The interesting part is the Clojure target, and the clean target.
<?xml version="1.0" encoding="UTF-8"?> <project name="bowlingforjunit" default="all"> <property file="bowlingforjunit.properties"/> <!-- Uncomment the following property if no tests compilation is needed --> <!-- <property name="skip.tests" value="true"/> --> <!-- Compiler options --> <property name="compiler.debug" value="on"/> <property name="compiler.generate.no.warnings" value="off"/> <property name="compiler.args" value=""/> <property name="compiler.max.memory" value="128m"/> <patternset id="ignored.files"> <exclude name="**/CVS/**"/> <exclude name="**/SCCS/**"/> <exclude name="**/RCS/**"/> <exclude name="**/rcs/**"/> <exclude name="**/.DS_Store/**"/> <exclude name="**/.svn/**"/> <exclude name="**/.pyc/**"/> <exclude name="**/.pyo/**"/> <exclude name="**/*.pyc/**"/> <exclude name="**/*.pyo/**"/> <exclude name="**/.git/**"/> <exclude name="**/.sbas/**"/> <exclude name="**/.IJI.*/**"/> <exclude name="**/vssver.scc/**"/> <exclude name="**/vssver2.scc/**"/> </patternset> <patternset id="library.patterns"> <include name="*.zip"/> <include name="*.war"/> <include name="*.egg"/> <include name="*.ear"/> <include name="*.swc"/> <include name="*.jar"/> </patternset> <patternset id="compiler.resources"> <include name="**/?*.properties"/> <include name="**/?*.xml"/> <include name="**/?*.gif"/> <include name="**/?*.png"/> <include name="**/?*.jpeg"/> <include name="**/?*.jpg"/> <include name="**/?*.html"/> <include name="**/?*.dtd"/> <include name="**/?*.tld"/> <include name="**/?*.ftl"/> </patternset> <!-- JDK definitions --> <property name="jdk.bin.1.6" value="${jdk.home.1.6}/bin"/> <path id="jdk.classpath.1.6"> <fileset dir="${jdk.home.1.6}"> <include name="lib/deploy.jar"/> <include name="lib/dt.jar"/> <include name="lib/javaws.jar"/> <include name="lib/jce.jar"/> <include name="lib/management-agent.jar"/> <include name="lib/plugin.jar"/> <include name="lib/sa-jdi.jar"/> <include name="../Classes/charsets.jar"/> <include name="../Classes/classes.jar"/> <include name="../Classes/dt.jar"/> <include name="../Classes/jce.jar"/> <include name="../Classes/jconsole.jar"/> <include name="../Classes/jsse.jar"/> <include name="../Classes/laf.jar"/> <include name="../Classes/management-agent.jar"/> <include name="../Classes/ui.jar"/> <include name="lib/ext/apple_provider.jar"/> <include name="lib/ext/dnsns.jar"/> <include name="lib/ext/localedata.jar"/> <include name="lib/ext/sunjce_provider.jar"/> <include name="lib/ext/sunpkcs11.jar"/> </fileset> </path> <property name="project.jdk.home" value="${jdk.home.1.6}"/> <property name="project.jdk.bin" value="${jdk.bin.1.6}"/> <property name="project.jdk.classpath" value="jdk.classpath.1.6"/> <!-- Project Libraries --> <!-- Global Libraries --> <path id="library.clojure.classpath"> <pathelement location="/Users/unclebob/projects/clojure-build/lib/ant-launcher.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/ant.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/clojure-contrib.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/clojure.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/commons-codec-1.3.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/commons-fileupload-1.2.1.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/commons-io-1.4.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/compojure.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/hsqldb.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/jetty-6.1.14.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/jetty-util-6.1.14.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/jline-0.9.94.jar"/> <pathelement location="/Users/unclebob/projects/clojure-build/lib/servlet-api-2.5-6.1.14.jar"/> </path> <!-- Modules --> <!-- Module BowlingForJunit --> <dirname property="module.bowlingforjunit.basedir" file="${ant.file}"/> <property name="module.jdk.home.bowlingforjunit" value="${project.jdk.home}"/> <property name="module.jdk.bin.bowlingforjunit" value="${project.jdk.bin}"/> <property name="module.jdk.classpath.bowlingforjunit" value="${project.jdk.classpath}"/> <property name="compiler.args.bowlingforjunit" value="${compiler.args}"/> <property name="bowlingforjunit.output.dir" value="${module.bowlingforjunit.basedir}/classes"/> <property name="bowlingforjunit.testoutput.dir" value="${module.bowlingforjunit.basedir}/classes"/> <path id="bowlingforjunit.module.bootclasspath"> <!-- Paths to be included in compilation bootclasspath --> </path> <path id="bowlingforjunit.module.classpath"> <path refid="${module.jdk.classpath.bowlingforjunit}"/> <pathelement location="/Library/junit4.6/junit-4.6.jar"/> <path refid="library.clojure.classpath"/> <pathelement location="${basedir}/classes"/> </path> <path id="bowlingforjunit.runtime.module.classpath"> <pathelement location="${bowlingforjunit.output.dir}"/> <pathelement location="/Library/junit4.6/junit-4.6.jar"/> <path refid="library.clojure.classpath"/> <pathelement location="${basedir}/classes"/> </path> <patternset id="excluded.from.module.bowlingforjunit"> <patternset refid="ignored.files"/> </patternset> <patternset id="excluded.from.compilation.bowlingforjunit"> <patternset refid="excluded.from.module.bowlingforjunit"/> </patternset> <path id="bowlingforjunit.module.sourcepath"> <dirset dir="${module.bowlingforjunit.basedir}"> <include name="src"/> </dirset> </path> <target name="compile" depends="clojure, compile.java" description="Compile module BowlingForJunit"/> <target name="compile.java" description="Compile module BowlingForJunit; production classes"> <mkdir dir="${bowlingforjunit.output.dir}"/> <javac destdir="${bowlingforjunit.output.dir}" debug="${compiler.debug}" nowarn="${compiler.generate.no.warnings}" memorymaximumsize="${compiler.max.memory}" fork="true" executable="${module.jdk.bin.bowlingforjunit}/javac"> <compilerarg line="${compiler.args.bowlingforjunit}"/> <bootclasspath refid="bowlingforjunit.module.bootclasspath"/> <classpath refid="bowlingforjunit.module.classpath"/> <src refid="bowlingforjunit.module.sourcepath"/> <patternset refid="excluded.from.compilation.bowlingforjunit"/> </javac> <copy todir="${bowlingforjunit.output.dir}"> <fileset dir="${module.bowlingforjunit.basedir}/src"> <patternset refid="compiler.resources"/> <type type="file"/> </fileset> </copy> </target> <target name="clojure"> <java classname="clojure.lang.Compile"> <classpath> <path location="/Users/unclebob/projects/clojure/BowlingForJunit/classes"/> <path location="/Users/unclebob/projects/clojure/BowlingForJunit/src"/> <path location="/Users/unclebob/projects/clojure/BowlingForJunit/src/bowling"/> <path location="/Users/unclebob/projects/clojure-build/lib/clojure.jar"/> <path location="/Users/unclebob/projects/clojure-build/lib/clojure-contrib.jar"/> </classpath> <sysproperty key="clojure.compile.path" value="/Users/unclebob/projects/clojure/BowlingForJunit/classes"/> <sysproperty key="java.awt.headless" value="true"/> <arg value="bowling.Game"/> </java> </target> <target name="clean" description="cleanup module"> <delete dir="${bowlingforjunit.output.dir}"/> <delete dir="${bowlingforjunit.testoutput.dir}"/> <mkdir dir="${bowlingforjunit.output.dir}"/> </target> <target name="all" depends="clean, compile" description="build all"/> </project>