1

Something is puzzling me after reading the this great answer to a related question:

There are two possibilities to share a function that I wrote in Clojure with Java developers

  • The first one is providing it in a JAR file so that they can call it as if I had written it in Java. Sounds great for Clojure advocacy.

  • The second one, the purportedly better way, requires those Java developers to use things like clojure.lang.IFn or clojure.lang.RT and invoking functions by passing their names as strings (!) instead of just calling them.

Why is the second approach "the better one"?

Jorge D
  • 41
  • 7
  • I think you're wrong, read that answer again. – m0skit0 May 30 '20 at 20:45
  • It's called using `tiny.binomial(5, 3)` – m0skit0 May 30 '20 at 20:55
  • @m0skit0: calling `tiny.binomials(5, 3)` is the "old way" - which is the one which I prefer. The "new way" is the one explained further below by @alex-miller with expressions like`IFn plus = Clojure.var("clojure.core", "+");` – Jorge D May 30 '20 at 21:22
  • *If you want to evaluate code from string*. Where did you read that this way is better? – m0skit0 May 30 '20 at 21:23
  • @m0skit0: Perhaps I have to re-word my question. In a nutshell: If I write a function in Clojure, e.g. `(defn foo ...)` I want my Java friends to be able to just import it and call it as if it were written in Java; i.e. `x = foo()` - I do *not* want them to do something like `RT.invoke("foo")`. Where did I read that this way is better? See what Alex Miller (top Clojurist) says: *As of Clojure 1.6.0, there is a new preferred way to load and invoke Clojure functions. This method is now preferred to calling RT directly (and supersedes many of the other answers here).* – Jorge D May 30 '20 at 21:40

1 Answers1

7

You are sorta setting up a false dichotomy here. Every approach involves creating a jar file: that is just how JVM programs are distributed. But there are 3 different ways for Java code to invoke Clojure code contained in a jar:

  1. Use methods in clojure.lang.RT to initialize the runtime, load files, and then look up vars. This is the old, deprecated approach.
  2. Use methods in clojure.java.api.Clojure to look up functions and invoke them. This is the newer version of (1), and hides some of the messy stuff you could accidentally get wrong.
  3. Use gen-class in the Clojure library to define a more Java-friendly interface to the Clojure functions.

You can still do (3) - there's nothing wrong with it exactly. But gen-class is a pretty clunky tool, and except for the simplest examples like exposing a number of static methods, it's just not a lot of fun, and it's not easy to provide an API that "feels" like a Java API using Clojure.

But you know what's great at providing an API that feels like Java? Java! So what I recommend if you want to make a Clojure library easy to use in Java is to include some Java code in your Clojure library. That Java code, written by you, bridges the language gap. It accesses your Clojure code by mechanism (2) above, and presents a Java-friendly facade so the outside world doesn't have to know there's Clojure underneath.

amalloy/thrift-gen is an example of a library I wrote years ago following this approach. It would not be at all easy to write this in pure Clojure, just because traditional Java idioms are very foreign to Clojure, and it doesn't support them all very well. By writing my own Java shim instead, Java clients get a very comfortable interface to work with, and I can just write Clojure that feels like Clojure instead of a bunch of gen-class nonsense.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • I guess that your recommendation is the best approach provided that you want (or have) to write Java code anyway. In my case, I _left_ Java long time ago (for Python) and the main point of Clojure (besides the beauty of the language) is _not having to write Java._ Just being able to expose a number of static methods may not represent the "full Java experience" (relishing in classes, interfaces, whatnot) - but is a good projection of the Clojure mindset as I understand it: dealing if possible only with plain functions (a.k.a. "static methods"). – Jorge D May 31 '20 at 09:19
  • If you don't care that much about Java consumers, just write your Clojure code as usual and let them access it via (2). gen-class stuff is just a headache, because it's like writing Java except you have to translate it to Clojure in this weird mini-language nobody ever uses. – amalloy Jun 01 '20 at 22:04
  • Why is the gen-class approach a headache? Provided that you are happy providing a static method: 1) write your Clojure function as you please 2) write a trivial two-line wrapper 3) "export" the function via `(ns...)` 4) slightly modify project.clj 5) build the (uber)jar ...and your Java friends can call that "static method" as if I had written it in Java P.S. thanks for your time! – Jorge D Jun 02 '20 at 21:12
  • Suppose your library were defining `clojure.core/map` (imagine it didn't already exist). What type should you give the function? Even if you just want to export something as innocuous as `nth`, its type sucks. Or if your function takes a map as input? There are just tons of totally normal Clojure functions you could write, which don't look good exposed directly to Java. Plus you don't have Javadoc! How will your Java friends even find out this stuff exists? – amalloy Jun 02 '20 at 21:16
  • Knowing that Java is statically typed I would design those Clojure functions to have quite "elementary" data structures as parameters (I think you can get very far with maps having just strings or numbers as keys - after all, there are no Objects in Clojure). This is a limitation I could live with - it's the expresiveness you would have e.g. with a JSON file as parameter. I am not conviced that gen-class is "clunky" or a "headache". Your javadoc argument, however, may point to the answer to my question: The recommended approaches apparently deliver a much better "Java experience". – Jorge D Jun 02 '20 at 22:42