2010-10-17

My thoughts on Clojure

While a lull in my thesis work allowed me to learn Go, I found myself with a couple more days where my thesis did not require a huge mental outpouring. As such I decided to see if I could learn Clojure before I had to start working on my slides for my defence on Monday.

So why learn Clojure? Having learned Scheme, Haskell, and OCaml (impressions) on top of treating Scala and JavaScript as functional languages, I already had familiarity with functional programming. In fact my functional programming experience stems from learning Scheme at Cal in their intro CS course along with being forced to use Common Lisp in an AI course there (although at that point I already knew C and Python). Having this much exposure to functional programming, I knew that I liked the style along with being comfortable with it. My dislike of Java but acceptance of the JVM (at least until Oracle sued Google) also has always motivated me to find JVM languages that can interface with Java easily.

It took me a total of three days to learn enough about Clojure to implement my core example scripts. In the end I liked the language, but not enough to want to put in the effort to do a semi-complicated example in the language.

First off, Clojure is a nice Lisp language. Scheme comes with too little in the language to use easily for much beyond teaching programming. CL is just like C++: way too much crap in a single language to be truly usable unless you make it your life's goal to master the language (and ignore everything bad about it). So compared to those two leading examples Clojure is great. The included data types are nice and the flexible metadata system is rather cool.

But (unfortunately) Clojure can't be viewed in isolation from Java. When you read the documentation for the language you will notice bits of Java method calls sprinkled throughout. For instance, to convert a string to an integer, the docs use (Integer/parseInt "42"). There is no Clojure-specific way of doing this very basic operation, so you have to realize Clojure is implemented on top of Java to get anything done. I found this slightly annoying as I wanted a language that could work with Java, but not that had to work with it. In other words Clojure is not simply a language implemented on top of the JVM but a language implemented on top of Java.

This connection to Java also turned out to be an issue with having clear error messages. For instance, I misread the docs for the ffirst function, thinking it took the second item from a sequence instead of being caar (if you don't know what caar is, don't worry about it). When I tried to do (ffirst [0 1]) to get at 1, the error message was like this:

Exception in thread "main" java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Integer (temp.clj:0)
      at clojure.lang.Compiler.eval(Compiler.java:5440)
      at clojure.lang.Compiler.load(Compiler.java:5857)
      at clojure.lang.Compiler.loadFile(Compiler.java:5820)
      at clojure.main$load_script.invoke(main.clj:221)
      at clojure.main$script_opt.invoke(main.clj:273)
      at clojure.main$main.doInvoke(main.clj:354)
      at clojure.lang.RestFn.invoke(RestFn.java:409)
      at clojure.lang.Var.invoke(Var.java:365)
      at clojure.lang.AFn.applyToHelper(AFn.java:163)
      at clojure.lang.Var.applyTo(Var.java:482)
      at clojure.main.main(main.java:37)
Caused by: java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Integer
      at clojure.lang.RT.seqFrom(RT.java:471)
      at clojure.lang.RT.seq(RT.java:452)
      at clojure.lang.RT.first(RT.java:540)
      at clojure.core$first.invoke(core.clj:53)
      at clojure.core$ffirst.invoke(core.clj:94)
      at user$eval1.invoke(temp.clj:1)
      at clojure.lang.Compiler.eval(Compiler.java:5424)
      ... 10 more

What line did the error happen at? If you don't know you need to ignore the traceback in the compiler that the error caused, you might think line 0 which is obviously wrong. But if you look at the triggering exception (the second one listed) you will notice it is in fact line 1, but only if you read down to the sixth line of the traceback. That gets really annoying really fast. You also need to realize that ISeq represents the sequence interface within Clojure along with knowing what java.lang.Integer is (which is obvious in this example, but not if it happened to be some obscure Java class).

And with all new languages, documentation was an issue. While it was great having documentation that explained the underpinnings of the language from the perspective of the reader and thus get into the nitty-gritty, there was a lack of documentation explaining in more introductory terms how to get stuff done. I had to read through the wiki (which is a WikiBooks book) to learn how to simply do concurrent execution the "right way" (answer: agents along with send). But even after that I had to figure out on my own that to prevent my code from hanging I needed to call (shutdown-agents) as none of the examples I read to figure all of this out made that necessary function call.

Clojure feels like a very good Lisp veneer over Java. But the problem for me is that it feels like a veneer instead of a fully free-standing language. At least with Scala the separation feels much more seamless so that it comes off more like Scala + JDK compared to Clojure + Java (and even then Scala feels like it does a more thorough job of marginalizing the need to know Java even more).