3 Things Java Programmers Can Steal from Clojure
The other day I wrote about some principles that programming in Clojure makes very clear. Those principles could be applied just as well in Java, and often are. However, there are some things that make Clojure distinct.
Three of those distinctions are the way it deals with state change (using an STM), the Persistent Data Structures, and the literal syntax for data with a reader (now called edn). Diving into the source code for Clojure, I realized that these three bits were written in Java. And that means that they can be used from Java. It is certainly not as easy as using them in Clojure, but they are all three powerful enough to warrant using them if you are using Java. You simply need to add one more JAR to your project (or add a maven dependency). I have constructed a few minimal examples of their use.
1. Persistent Data Structures
Clojure comes with several powerful and fast collection classes. The
interesting thing about them is that they are
immutable.
If you want to add an object to a list, you actually create a new list
containing the old elements and the new element. Instead of using
copy-on-write, it reuses most of the internal structure of the original
list, so only a small number of objects need to be allocated. It turns
out that this can be done very quickly, comparable to using an
ArrayList
.
The following example illustrates three of the more useful data structures: Vector, HashMap, and HashSet.
package persistent;
import clojure.lang.IPersistentMap;
import clojure.lang.IPersistentSet;
import clojure.lang.IPersistentVector;
import clojure.lang.PersistentHashMap;
import clojure.lang.PersistentHashSet;
import clojure.lang.PersistentVector;
public class PersistentTest {
public static void main(String[] args) {
IPersistentMap m = PersistentHashMap.create("abc", "xyz");
m = m.assoc(1, 4); // add a new key/value pair
m = m.assoc("key", "value");
m = m.without("abc"); // remove key "abc"
System.out.println(m);
IPersistentVector v = PersistentVector.create(1, 2, 3);
v = v.assocN(0, "a string"); // change index 0
v = v.cons("should be last"); // add a string at the end
System.out.println(v);
IPersistentSet s = PersistentHashSet.create("a", "b", "c");
s = (IPersistentSet) s.cons("d"); // add d to the set
s = (IPersistentSet) ((IPersistentMap) s).without("a"); // remove an element
s.contains("g"); // should return false
System.out.println(s);
}
}
Now, it ain't pretty. But it's actually no worse than quite a few native Java libraries I've seen. There may be a better way to do this, but this one works.
2. Software Transactional Memory
Clojure uses Multiversion concurrency control to provide a safe way to manage concurrent access to state shared between threads. In Clojure, they are called refs. I won't go very deep into how it works. Suffice it to say that Clojure refs gives you non-blocking reads and transactional updates without having to do locking yourself. There are two caveats: 1 is that the value you give to the ref has to be immutable. 2 is that you should not perform IO (or perform any mutation) inside of the transaction.
package stm;
import java.util.concurrent.Callable;
import clojure.lang.LockingTransaction;
import clojure.lang.Ref;
public class STMTest {
public static void main(String[] args) {
// final needed to be used in anonymous class
final Ref r = new Ref(1);
final Ref s = new Ref(5);
try {
// run this in a transaction
// don't do IO inside
LockingTransaction.runInTransaction(
new Callable<Object>() {
public Object call(){
s.set((Integer)r.deref() + 10);
r.set(2);
return null;
}
}
);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(r.deref());
System.out.println(s.deref());
}
}
3. Extensible Data Notation
With Clojure 1.5, edn has become a standard part of the language. Edn is like an extensible JSON where the keys of objects can be any value (not just strings). It is based on the Clojure literal syntax, much in the same way that JSON is based on Javascript literal syntax. It is a nice way to serialize data. And since you already have the JAR in your project, it's a no brainer to use it.
package edn;
import java.io.PushbackReader;
import java.io.StringReader;
import clojure.lang.EdnReader;
import clojure.lang.PersistentHashMap;
public class EDNTest {
public static void main(String[] args) {
// reading from a string
System.out.println(
EdnReader.readString("{\"x\" 1 \"y\" 2}", PersistentHashMap.EMPTY));
// reading from a Reader
// really, you can use any Reader wrapped in a PushbackReader
System.out.println(
EdnReader.read(new PushbackReader(new StringReader("#{10 2 3}")),
PersistentHashMap.EMPTY));
}
}