> If we used interop for everything mundane, Clojure would really just be an S-expression shaped husk over Java code. Not a very good solution.
I don't have a lot of experience with Clojure, but speaking as someone who uses Kotlin on the server side, this attitude strikes me as odd. Most of the reason to use a JVM language is to take advantage of the mature and well-maintained Java libraries for basically everything.
If interop is considered a last resort (to the point where you reach for a 3-year-old unofficial Stripe library over the officially maintained Java one), what's the point of running on the JVM at all?
To expand a little on this, here's Rich Hickey on the subject [0]:
> I dislike writing Java/C# as much as anyone, and Clojure is my ticket
to writing much less of it, while leveraging the efforts of
multitudes. As I've said in my talks, most Clojure users go from "eww,
Java libs" to "ooh, Java libs", leveraging the fact there there is
already a lib for almost anything they need to do. And using the lib
is completely transparent, idiomatic and wrapper free. OTOH, if they
want to customize their interface to a lib, it is trivial to do so in
Clojure.
> If, however, the stigma of Java extends to libraries written in it,
even when consumed from languages like JRuby and Clojure, and people
think it's cooler to reinvent than reuse, they need a new definition
of cool.
It Kotlin it makes a lot more sense to use Java libraries, because for most purposes, the interfaces you'd write in Kotlin and the ones in Java are not all that different, other than coroutines. From Scala or Clojure, the differences in interface design are far larger. You are going from languages where mutable state is an optimization you want to hide, to libraries full of setters that return void. A land of monads, and maybe macros vs imperative programming. This makes the ergonomics of many mature java libraries just too much of a bother to use directly. There are cases where the effort to rewrite is bonkers (say, ApachePOI), and others where wrapping is sensible (see a variety of json serialization libraries which support different parsing backends, some of which will be straight java), but there are many libraries that are sensible from Kotlin, but are worse than a rewrite if you are using an FP language.
Still, I think you are undervaluing the JVM itself, which is almost magical when it comes to production observability. It'd be a whole lot of work to come anywhere near that tooling support for ops if you were avoiding the JVM. Scala, for instance, can also transpile into Javascript if you feel like it, and there's a native project that compiles to LLVM, but the JVM has the vast majority of the use cases, and it's not due to the libraries.
Yeah, I get that. Even in Kotlin there definitely a number of places where I prefer the Kotlin library to the Java, and I can see that it would happen more often in Clojure.
That said, the example in the article is the Stripe API, and I cannot see a good reason not to just use the official Java one. I've used it a ton, and it's as simple as can be—you pass a map in and get back an object. If that's a problem, either Clojure has a way worse interop story than necessary or the author is suffering from a variant of not-invented-here syndrome.
There is a difference between a library and a framework, framework dictate how you work, library is an external, isolated to some extent, entity. I can see why someone would like to have a more native framework but will be ok to use an external library for purposes like stripe.
Agreed, but again, the author isn't actually talking about frameworks except at the very end. Most of the article is about API wrappers like Stripe:
> My theory is, this causes a lack of effort expended on mundane libraries. It's not the most fun thing to build. I get it. Really, I do, but lack of these API-wrapping and protocol-handling libraries put Clojure at a significant disadvantage in the business of software.
The kinds of libraries the author misses in Clojure exist in abundance in Java, and the most likely explanation for their absence in Clojure is that most Clojure developers don't avoid interop as much as the author thinks they do.
When I started out with Clojure, I was using wrappers everywhere, but after about a year or so realised that they added very little value, hurt performance, and made the code less simple. On top of that, wrapper libraries were often not complete in functionality.
The pivotal moment for me with this was when dealing with Kafka, just using the “raw” Java library ended up being so incredibly much simpler than any of the wrapper libraries.
It's been more than a decade since I've used Clojure so I say this with a big grain of salt.
Where there weren't good wrappers, just a few functions "wrapping" Java libraries for my project's needs always felt lightweight and lispy. The little internal APIs I made almost always passed around the library's raw Java objects, but with more Clojure idiomatic ways to construct them, or slapping some seq on the library outputs.
I have a github repo where I implement a feature identical microservice in various tech stacks. I started that repo with a Clojure version that used community provided wrappers. See https://github.com/gengstrand/clojure-news-feed/blob/master/... as an example of calling Cassandra. Recently, I added another implementation with Clojure that just called the Java drivers directly. See https://github.com/gengstrand/clojure-news-feed/blob/master/... for that version of the same call. In the end, I decided to forego wrappers and make the calls directly because you end up with fewer dependencies and are more likely to be able to use latest versions of everything.
Really it's not. The interop is a conscious decision core to the language rationale.
https://clojure.org/about/rationale
> Customers and stakeholders have substantial investments in, and are comfortable with the performance, security and stability of, industry-standard platforms like the JVM. While Java developers may envy the succinctness, flexibility and productivity of dynamic languages, they have concerns about running on customer-approved infrastructure, access to their existing code base and libraries, and performance
> symbiotic with an established Platform
> VMs, not OSes, are the platforms of the future, providing: [...]
* Libraries
* Abstract away OSes
* Huge set of facilities
* Built-in and 3rd-party
Where Clojure had nothing to bring, nothing was implemented, because interop is considered just as good, that's why there's not a lot of IO functions in core, and why math was missing for a long time.
A good wrapper (like the Clojure version of java-time) can be a real lifesaver for those of us without Java backgrounds who don’t like having to learn deep and often baroque Java apis.
I also use a pretty half baked wrapper someone wrote because I just about lost my mind trying to make sense of the class hierarchy it was wrapping.
I think for people like Rich and Alex wrappers may look pointless because they already have invested in learning many of these APIs, know the patterns already, etc. For people like me from different language backgrounds they keep me from having to learn APIs with mutable state and deep class hierarchies and to me that’s a big win.
(I rewrote this whole comment to hopefully be more concise.)
Very good point. I have no rebutal, you're absolutely right on that front.
I just don't know how to address this problem. It seems the people in a position to build good wrappers or Clojure implementations are also the people who know Java and Clojure, and maybe came to Clojure from Java. For them, it's easy to interop directly, so they take the path of least effort, and don't bother making a Clojure lib.
In a way, if Clojure couldn't use Java, while it would mean I probably wouldn't be able to use it at my work and would be forced to still work in Java, it would also mean maybe for my personal stuff or for certain companies who'd embrace it anyways, I would and they'd be forced to go ahead and put the effort to build Clojure variants of all these libraries, and that would make it friendlier to people learning Clojure that don't know Java.
I have to admit, I always react a bit defensive on articles like this one, and it's mainly because I don't see the problem, I see people complaining about things that are non-issue for me. But your point hits me in a different way, you make a good point, having to learn a language and a half (all of Clojure and half of Java) is definitely not as good as it should be.
I think normally people come at it like saying how interop is bad, difficult, not idiomatic, or that there's no libraries so they had to use Go-lang, where there were clearly options to use a Java lib from Clojure. But if the article just said, hey, using Java is unapproachable to a lot of folks, learning to understand Java APIs and semantics is a lot of work, and it's turning people away from Clojure otherwise, now that's something worth discussing.
Ya I hear you and agree with most all of this. I did choose Clojure in part because it had Java interop so on some level I knew I’d need to learn a bunch of Java and APIs to take advantage of that. I think it’s a great design decision but also am actually kind of a fan of wrappers - I think being able to use raw Java is great and sometimes the right choice but I also appreciate even the half baked wrappers sometimes. As to the original article I should probably read it :-) I was jumping into the discussion more to address this chip on my shoulder about wrappers.
Just a nit-pick: clojure.java-time wraps Java's own _immutable_ java.time.* classes and it really doesn't provide much value add. I initially thought it did and introduced it at work but we're taking it out of a lot of our code now and using plain interop -- it's only slightly more verbose, it's already mostly "functional" in style, and it gives us more precise control over conversions between different types of date/time values.
Your main point about interop as a barrier to non-Java people is reasonable -- although if you learn a hosted language, you're going to have to learn stuff about the ecosystem on which it is hosted (so, not Java itself, but some of the JVM stuff and the docs for any libraries you interop with). And this is true of the JS ecosystem supporting ClojureScript (and the .NET ecosystem supporting ClojureCLR).
> although if you learn a hosted language, you're going to have to learn stuff about the ecosystem on which it is hosted
I thought this at first, but then I thought if you take Python, which is hosted on the OS directly through CPython, so we can say Python is hosted on Windows or Linux.
Well it turns out, most people never need to interop directly with Windows or Linux APIs or any other C interop.
Python seems to have a different mindset here, where most of its value proposition are providing wrappers over C/Windows/Linux/Mac APIs.
Why does so much effort get put in maintaining all these wrappers in Python? Is it just attitude? Is it that those APIs are much worse then Java's enough that even C programmers using Python want to wrap them over using them directly?
This is the most illustrative analogy. I suppose the answer is a little bit of both.
Python numeric performance is a really interesting case. Those folks would certainly drop down to C. But my casual observation is that they would build the bridge when they came back above water. A lot of work went into this because Python performance has historically been quite bad for a Data Science/ML language.
Part of this is the size of the community.
But what is we also consider the Common Lisp community? Common Lisp is incredibly performant. A lot of work went into the underlying C libraries. Perhaps most of that was when the community was more vibrant. But still, a casual observation is that CL folk live in CL.
Thanks, I’ll check out the raw java-time next time I have a minute.
You’re right about needing to learn some of the host in a hosted language. I’ve enjoyed for example Jsoup and I use some of the Math stuff inline with no issue. Even with the java time wrapper I’ve dropped down to the original to do certain things involving locales. I definitely knew going in to Clojure I’d need to pick up some Java.
Where I really like wrappers is where the underlying java API is not great in some way. (Which now that I’ve seen the original puredanger quote seems to be what he’s saying.)
> Really it's not. The interop is a conscious decision core to the language rationale.
I'm aware. I was expressing an opinion to a twitter friend, but I should have foreseen the replies of some people taking my opinions as me promoting fact. That's my fault for not being clear.
Clojure is intended as a hosted language and embracing the host is the philosophy, so interop with the host is never a bandage.
Again, I am curious where you get all these impressions that are contrary to most people in the community take for granted, such as:
* Clojurians prefer libraries over framework, but you said we spent too much effort in developing "big web framework".
* Clojure is intended to be a hosted language, so seamless interop with the host is the norm, but you thought interop is a bandage until Clojure matures.
“Idiomatic Clojure code calls Java libraries directly and doesn’t try to wrap everything under the sun to look like Lisp… Where Java isn’t broken, Clojure doesn’t fix it.”
Perhaps that stance has changed over time. In Clojure, there's the clojure.string namespace just functions wrapping java.lang.String methods https://clojuredocs.org/clojure.string
This is much more than "just wrapping". It allows strings to be used more readily with existing Clojure abstractions and/or allows idiomatic use without the need for being on guard to avoid reflection.
Notably, Alex didn't write this library and only has a minor contribution to it.
There's two cases where you'll see pragmatic wrapping:
1. Higher order usage. When functions would really benefit from being used in a higher order way, passed around as values, you'll see some of the Java methods wrapped so they can more easily be used as such, since you can't use Java methods in a higher order way. This would count as "can do it better", and it's the case for clojure.string.
2. For portability with Clojurescript. This is a more recent thing, but as Clojurescript usage grows, and code reuse between backed and frontend more common, there are certain basic methods Java interop was totally fine with that now benefits from being wrapped. This is the case for clojure.math.
I think the problem is that if you want to write code portable between clj and cljs, you’re gonna need a lightweight abstraction. With that said, I’m still not sure how exactly the maintainers feel about portable code, having written a portable http library.
Why does he say this? I would think the opposite and in a professional environment I've gone in the other direction of regretting not writing _more_ wrappers for external libs.
You should take Alex's words contextually, and with a grain of salt. You can easily see his commits in this project, for example: https://github.com/cognitect-labs/aws-api
This library doesn't wrap the AWS SDK, explicitly calls out existing Clojure wrappers that do in the README, and none of his commits seem to conflict with the referenced Tweet. Am I missing something?
Interop with Java libs is decidedly not pleasant in many cases. I haven't used Clojure before, but I've worked on services built in Scala and JRuby over the years.
There are a few challenges you run into:
1. Many libs written in Java follow the worst of Java's design patterns and idioms. Rather than calling a function and passing it some data as an argument, you instead have to instantiate a WhateverClient that itself takes a WhateverThingyStrategy instance and a WhateverConfig class instance, then call a .executeCommand method with a new IWhateverCommand anonymous class instance that implements the execute, doCatch, and doFinally methods. And of course, you can't forget whatever subclass of AbstractOrigamiWhateverResponseMarshaller you need next. Oh, and because all these instances need to be defined as beans that will be DI-friendly later, there's even more boilerplate to look forward to.
2. In comparison to languages like Scala, Java's type system is often quite lacking. You can never trust that a non-Option type is not null.
3. If the rest of your code is free-floating with structural types in some other language, you inevitably need to write a ton of glue code for adapting these structural data type objects to whatever blessed POJO interface Java expects.
All that being said, I still would prefer to use the more mature Java lib if the more native choice isn't as well maintained. But there's no denying that it's painful. The ergonomic hit is just as bad (if not more than) execing a child process to run code in another well-suported target like Rust/Go/Node/whatever that maybe has more sensible idioms. There are of course a lot of reasons why that'd be a bad idea, but still...
> The ergonomic hit is just as bad (if not more than) execing a child process to run code in another well-suported target like Rust/Go/Node/whatever that maybe has more sensible idioms. There are of course a lot of reasons why that'd be a bad idea, but still...
Come on, we both know this is false and it is only your feeling, nothing factual. How is IPC with all of its intricacies anywhere close to as ergonomic as calling one more method in the host language when all of your objects are also of said host language?
Frankly enough, creating a wrapper should not be hard at all, no matter the language. Especially not for libs that are mostly unidirectional in communication.
> Come on, we both know this is false and it is only your feeling, nothing factual. How is IPC with all of its intricacies anywhere close to as ergonomic as calling one more method in the host language when all of your objects are also of said host language?
Oh, it's definitely not false. The reality is you can't just call one more method in many cases. There is a ton of ceremony involved because enterprise quality coding[0] is often seen as a desirable attribute of the software written in Java. The linked example is an exaggeration, but it's sadly not too far off from reality in some cases.
I'd also counter that IPC is often dead-simple in many contexts. Sure, a long-lived process like a server might have intricacies you need to consider, but outside of that, it's pretty straight-forward. CLI tools are another example of this, but the JVM startup times generally make it a bad choice for CLI tools to begin with.
> Frankly enough, creating a wrapper should not be hard at all, no matter the language. Especially not for libs that are mostly unidirectional in communication.
This depends on the surface area of the library you're using. And maintaining such a wrapper is not a one-off activity. It's not an issue of being "hard", but tedious and time-consuming.
> Sure, a long-lived process like a server might have intricacies you need to consider, but outside of that, it's pretty straight-forward.
The rest of your comment is fluff around this admission that the person you are responding to is correct. I'm sure you're aware this is the primary use-case for Clojure.
In the very very worst case, you can just write a saner Java wrapper over the Java lib (chances are you are not needing every option in the original), and use that from the guest language of your choice.
One problem with interop is you import the design/ergonomic considerations that may come from other languages, while neglecting what is uniquely powerful about your own language.
I don't have a lot of experience with Clojure, but speaking as someone who uses Kotlin on the server side, this attitude strikes me as odd. Most of the reason to use a JVM language is to take advantage of the mature and well-maintained Java libraries for basically everything.
If interop is considered a last resort (to the point where you reach for a 3-year-old unofficial Stripe library over the officially maintained Java one), what's the point of running on the JVM at all?