One particularly powerful aspect of Clojure is that it allows you to use Java features without actually writing any Java. And the best way to get my head around the Java interop features is to do a practical example; something like I'd do for work. And as far as examples go, they don't come any more practical than CSV file reading.
Given I'm just learning Clojure, to have any chance of getting the interop code working, I first need to write the Java version. So how would a Java person, which I'm not, parse CSV? Using Apache Commons CSV I suppose. Here's the CSV:
id,greeting
1,Shiver me timbers!
2,Ahoy there!
CSV parsing in Java
Here's the Java code, PiratePhrases.java
:
import java.io.FileReader;
import java.io.IOException;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
public class PiratePhrases {
public static void main(String[] args) throws IOException {
FileReader reader = new FileReader("pirate.csv");
Iterable<CSVRecord> records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader);
for (CSVRecord record : records) {
System.out.println(record.get("greeting"));
}
}
}
As I'm running Guix SD, I'll install Java and Commons CSV and compile and run it like this:
$ guix package --install icedtea java-commons-csv
$ javac -cp "${HOME}/.guix-profile/share/java/commons-csv.jar" PiratePhrases.java
$ java -cp "${HOME}/.guix-profile/share/java/commons-csv.jar" PiratePhrases
Shiver me timbers!
Ahoy there!
I've barely written an Java since university, and don't use any statically typed
languages at the moment so it was comforting to that javac
demanded I handle
IOExceptions
; even if I did fob it off by adding the exception to the
interface.
CSV parsing in Clojure
Any sane Clojure programmer would probably reach for a Clojure native CSV
library, like data.csv, but since this is
an exercise, I'm going to stick with Commons CSV and translate this Java code as
literally as I can. The Clojure version, pirate_phrases.clj
, looks like this:
(import java.io.FileReader)
(import org.apache.commons.csv.CSVFormat)
(import org.apache.commons.csv.CSVParser)
(import org.apache.commons.csv.CSVRecord)
(let [reader (FileReader. "pirate.csv")
records (.parse (.withFirstRecordAsHeader CSVFormat/DEFAULT) reader)]
(doseq [record records]
(println (.get record "greeting"))))
And, on Guix SD, is run like this:
guix package --install clojure
java -cp "${HOME}/.guix-profile/share/java/clojure-1.9.0.jar:${HOME}/.guix-profile/share/java/commons-csv.jar" clojure.main pirate_phrases.clj
Shiver me timbers!
Ahoy there!
This was a good chance to get familiar with a few of the interop features:
-
Importing with
(import java.io.FileReader)
-
Instantiating an object, where
new FileReader(path)
becomes(FileReader. path)
-
Accessing a class attribute/method, where
CSVFormat.DEFAULT
becomesCSVFormat/DEFAULT
, and -
Calling an instance method, where
record.get("greeting")
becomes(.get record "greeting")
.
I particularly like that the Clojure notation clearly distinguishes class
attributes/methods from instance methods eg. CSVFormat/DEFAULT
vs. (.get
record "greeting")
.
I'll still be reaching for Python when a CSV file comes my way, but I was pleasantly surprised at how neat the Clojure version of this code ended up.