Don't really get the criticism of Clojure for being hosted on the JVM, particularly relative to its status as a "productive" Lisp. Like oh, you get access to one of the biggest and most mature library ecosystems out there as well as best in class operational tooling? Obviously there are use cases where the JVM doesn't fit and all things being equal I prefer shipping statically linked binaries too, but the JVM still feels like an obvious "pro" here.
> Don't really get the criticism of Clojure for being hosted on the JVM, particularly relative to its status as a "productive" Lisp.
If you are doing long running server side apps, it is a better fit. Even better if you are already a Java shop.
Otherwise, its either detrimental or, at the very least, a source of very 'alien' behavior, not the least of which being the stack traces. That gets pretty obvious when you compare with the likes of Common Lisp with its incredibly elegant system that's essentially Lisp _almost_ all the way down.
The JVM has its own advantages of course. Billions of dollars of optimization work being one of them. Being able to use java libraries to fill the gaps (at the expense of the less elegant stack traces and some
It can be a complete show stopper in many applications. Say, you want to interface with C libraries. Or embed some form of Lisp in your app. Browser-based apps (emscripten doesn't help you), which is why Clojurescript exists.
Or you are building something like an iOS application. I have successfully embedded (although not shipped to the App store) Chicken Scheme in a couple of different ways. The first, as a library with all the cross compilation nonsense. And the second, by simply telling the compiler to stop at C code generation, adding the C blob in the app, and compiling everything together with the rest of the app. That gave me a remote REPL which was amazing for debugging.
Fair points, I say. But interfacing with C isn't a show stopper is it? Clojure can bridge to C by exposing the C library though the JNI/JNA framework.
It isn't much fun and there are certainly situations where I'd say Clojure was a poor choice for calling C/C++ libraries, but if you need to do it then it can be done.
I hardly wrote any Clojure, but the only thing that bugged me was the startup time of the repl. It's been talked about enough. Yes, that problem goes away if I use a proper setup with a language server or whatever, and yes it doesn't matter for "situated" production applications, but it still peeved me.
What do I care if it's in the JVM? Sure, a JVM instance uses a lot of memory to help the garbage collector, but that doesn't bother me. JVM is just an old, mature ecosystem. Every runtime we work with (browser, nodejs, CPython, your decades of hand-written C++, the Go standard library) shares design tradeoffs with the JVM. Nothing inherently off-putting about it.
I haven’t touched JVM in ages, but there are two things off putting about it.
First it’s viscerally slow. They have a state of the art GC, amazing benchmarks, tons of work going into performance, but it still feels slow and laggy when you develop on it. None of the other ecosystems you mentioned have that problem (including Python).
Second, they have a bad sense of design. The class library comes from a culture of needing three classes to open a file, and that culture permeated through the entire ecosystem. Almost all the software in it feels bloated and over engineered. The modal JVM experience is spending 95% of your time dealing with “enterprise-y” boilerplate that turns out to have nothing to do with the enterprise and everything to do with bad design decisions and the culture downstream from those. C++ has its own flavor of this problem, but certainly not Python or Go.
I couldn't agree more. I'm not very knowledgeable on Java, but was blown away every time I looked to see the crazy amount of boilerplate to do anything. There are all these design patterns that seem to only exist because the language is so terrible. Thousands of people who aren't professional developers write millions of lines of Python each year (just a guess, but sounds right) and the vast majority just write code and don't need 50 classes in their application to do something.
You're talking about something different - Java the language is a bit ugly, but this is about JVM performance (ie, the runtime virtual machine that is installed to execute Java programs) with Clojure, where there is not much boilerplate to speak of.
Although the JVM is one of the sleekest environments around though and I'm confused by the fellow saying it is "viscerally slow". Clojure loads slowly, but after that everything happens at speed.
> First it’s viscerally slow. They have a state of the art GC, amazing benchmarks, tons of work going into performance, but it still feels slow and laggy when you develop on it. None of the other ecosystems you mentioned have that problem (including Python).
I really can't relate to this. What part of the process is noticeably slower than Python? I have a lot of Python projects and couldn't say any of them are slower than JVM apps.
It does preclude it, but clojure found an arguably elegant solution to it, using recur[1] instead. As a plus, in addition to achieving the same result as tail-call elimination, it does check that the call is indeed in tail position, and also works together with loop[2].
For me, it made me not miss tail-call elimination at all.
the call to bar is a tail call. How does recur optimize this? Well, it doesn't, since "general TCO" / "full TCO" means that any tail call gets optimized, such that the stack does not grow. Clojure recur/loop is just a loop notation.
Looping construct in most languages provides a simple conversion of self recursion (a function calls itself recursively) to a loop: update the loop variables and then do the next loop iteration.
But the general case of tail calls is not covered by a simple local loop, like what is provided by Clojure.
The issue arises when you program really heavily with closures and function composition. You sadly cannot do functional programming as in "programming with functions" without care on the JVM.
It is, IMO, a missed opportunity to use a hard-coded identifier for `recur`ing instead of the `(let sym ((...)) ...)` form that would let you nest loops.
Aside from that, I agree. Tail-call optimization's benefits are wildly overblown.
The benefits aren't overblown if you are someone who learned Lisp with a functional approach. As in, using higher-order functions etc. You have to be careful whenever you approach a problem that way on the JVM.
what does tail-call optimization have to do with higher-order functions? I thought the former pertains to iterative procedures written with recursive syntax, where the recursive call is at the very end of the function and called by itself, so stack size is O(1). Higher-order functions means passing functions to things like map, filter, etc.
In the context of higher order functions, tail call elimination allows for the avoidance of building up intermediate stack frames and the associated calling costs of functions when doing things like composing functions, particularly when calling large chains of nested function calls. The benefits of TCO for something like mapping a function can also be pretty large because the recursive map can be turned into a while loop as you describe at the beginning of your comment.
The optimization of stack frame elision is pretty large for function calls on the JVM and the stack limits are not very amenable to ‘typical’ higher order function ‘functional programming’ style.
> tail call elimination allows for the avoidance of building up intermediate stack frames and the associated calling costs of functions when doing things like composing functions, particularly when calling large chains of nested function calls.
This is more general than what tail-call-optimization can handle. This is true, but only in the context of recursive functions, and you don't actually save anything besides not needing to re-allocate the stackframes below your recursion point. Other optimizations such as inlining may perform some of this in the general case. Regardless, you get the same benefits by using `recur` in Clojure, it's just explicit, it still uses no extra stack space.
The downside is purely stylistic. It's functionally the same as if you did `(let recur () ...)` in Scheme.
I’m not certain, but I am pretty sure tail call optimization includes generic tail call elimination. This does not rely on the function being recursion. In effect the compiler converts all tail calls into direct jumps and allows for the reuse of stack space, limiting the total stack size for any given chain of tail called function to a statically determined finite size. This same optimization also allows for the omission of instructions which manage stack frames and their associated bookkeeping data. I know the Ocaml compiler does this, and I’m almost sure that GHC does as well.
I do not know if the above is included in what clojure does for tail calls, recursive or not, but on the JVM the elimination of those calls can and does have an impact.
> I’m not certain, but I am pretty sure tail call optimization includes generic tail call elimination.
I believe they're related, but not the same thing.
> I do not know if the above is included in what clojure does for tail calls, recursive or not, but on the JVM the elimination of those calls can and does have an impact.
As far as I know the JVM doesn't allow a program to arbitrarily modify the stack, so any support would need to be baked into the JVM itself, which it might be now, but I'm not finding any indication that it is. The `loop`/`recur` construct essentially compiles to a loop in the Java sense (to my understanding), so it is as efficient as a recursive loop with TCO. The more general tail-call elimination likely isn't possible on the JVM, but you're correct that it would likely result in a speed up.
All of this is sort of besides the point: I don't think there's much in terms of higher-order functions (which is an extremely broad category of things) that you can't do in Clojure just because it lacks TCO. At least no one has been able to give me an example or even address the point directly. Speed is not really what I'm referring to.
If you program purely and represent "state" as a function
s -> (a,s)
Then the JVM bites you the moment you naively abstract over that. You end up having to manually "compile" stuff like traversing a list with a stateful update. For example, Scala's pure FP ecosystem is full of specialized functions for state due to this.
I think hosted is almost tautologically a mixed bag - you get access to the host (both +ves and -ves) but you also introduce a layer and some invariable friction.
And quite a lot of people actually do ship statically linked binaries with Clojure, using GraalVM. Clojure LSP server for example is distributed as a static binary.
Yes, although that solution can't yet be said to be "push button". My impression is that there are a decent amount of people who want non-JVM, native Clojure. Hence efforts like Janet and Jank.
I would have dumped lambda as a runtime before dumping the codebase. Obviously we don't know the full story but that sounds a lil silly to me.
Plus lambda's will stay alive for quite a while if they are in use. So startup time is felt once, but then would be identical to any other language or runtime.