I recently had a chance to start a greenfield project, after having supported a team using Clojure for applications development for over 10 years. I was surprised to find myself more productive in plain contemporary Java.
A decade ago, Clojure’s functional language features were a huge differentiation and worth taking a small hit in runtime performance in exchange for lightweight code. Java 6 was a painful exercise in verbose boilerplate programming.
And while Clojure is remarkably stable, and our software never “rotted” due to changes in the language, recent releases of Java have shown great progress in innovation which Clojure is slow to support.
A specific case is the “single abstract method interface” pattern that Java supports via lambda expressions. Important Java libraries, like the AWS SDK, use these types of interfaces a lot and they’re just awkward to program against using Clojure / Java interop. And this feature was introduced in Java 8, iirc which is now 10 years old!
10 years ago I had to evangelize and advocate for the choice of Clojure over other JVM languages, and I believe the last decade of productivity and business value has proven me correct. But I would find it hard to hold that position against the Java of today.
I have a hard time figuring out what makes good contemporary Java. My knowledge is mostly stuck in Java 5.
A big part of my gripe is boilerplate, and I don’t see that gone.
“single abstract method interface” seems interesting. I normally don’t bother with Java interop, preferring libraries that have solved this interop for me. Those libraries can be thin layers, but it’s still nice.
Meanwhile I’m still coming to grips with the performance penalty. There are ways to optimize Clojure, but I’ve not found them necessary… but am aware my code is less than the most performant.
Here are a few of my observations how Java has materially reduced boilerplate:
The SAM interface pattern (since Java 8) I mention above means that simple functional routines can be written in a single line of code, where it used to require declaring a new class.
The java.util.function and java.util.concurrent (since Java 5, but with many more useful APIs introduced later) packages use SAM types to make complex functional (and asynchronous) transformations easy and concise. Not as terse as Clojure’s -> threading macro, but not terribly far apart either.
The var keyword (since Java 10) for local type inference means you can get away with omitting type declarations pretty much everywhere except the function signature, yet still take advantage of Java’s strong static type system.
The record class declaration (since Java 16) for immutable datatypes.
In practical use, these features let me write a submodule that deals with S3 via the AWS SDK in about 125 lines of Java. (All in, including imports, comments, logging, and all!) The Clojure version wasn’t appreciably shorter, but I also had to implement my own thin library layers to deal with the impedance of the AWS SDK.
Also jshell (since Java 9) is an acceptable REPL.
Edit to mention: Clojure’s IDE support absolutely sucks, and combined with the lack of enterprise-grade frameworks like Spring Boot, it’s still a really hard sell in a large company.
I don’t have a lot of experience of either modern IDEs or Clojure but I’m surprised to hear that. I thought REPL integrations were one of the killer features of Lisps: isn’t an editor with a REPL integration like a super-IDE?
enterprise-grade frameworks like Spring Boot
This might not be an easy question to answer in a forum comment but what is it that makes a framework “enterprise-grade”? As an outsider looking in, I’ve never really understood what something like Spring Boot is actually for.
I don’t have a lot of experience of either modern IDEs or Clojure but I’m surprised to hear that.
The problem is this statement could mean a number of things:
Support for Clojure within “IDEs in general” is very poor. This one is probably true? IntelliJ is the only major IDE I know with actively-maintained support. If you pick a random IDE and measure it’s Clojure support, it’s very likely to be bad!
There exist no IDEs with good Clojure support. Not true. IntelliJ’s Clojure support isn’t as good as Emacs’s, but from my experience pairing with co-workers, I’d still say it’s good.
Features which are commonly considered “IDE features” don’t work well in Clojure. This one … is complicated. You can use clojure-lsp to get a lot of fancy static analysis features that work well on code… right until you call a macro, and then all bets are off. Static analysis tooling refuses (reasonably-ish?) to actually expand macros, so any macro which binds locals is going to give unreliable results.
The last point is interesting, ‘cause I’ve found elixir-lsp to work very well with macros in general in Elixir, which is very macro heavy. I don’t know if it expands them, but it offers docs and autocompletion and highlights reasonably sensibly when a failure occurs in them and so on. Not familiar enough with either to really know whether they do something different though, or just elixir-lsp fails gracefully enough I don’t really pay attention to it.
I’d personally consider any macro that binds locals outside its own scope to be a huge footgun waiting to happen anyway, though that may be my Scheme/Rust showing through. Unless it’s the sort that’s literally generating boilerplate functions for you, I suppose.
I’d personally consider any macro that binds locals outside its own scope to be a huge footgun waiting to happen
I agree! I’m talking about macros which bind locals within their body. For instance, at work we have a testing macro that sets up a test-mode system and binds it; for instance:
This expands to a let block where system is bound as a local inside the body.
In Clojure, you can’t safely expand with-system in static analysis, because it could run arbitrary side-effects, including erasing your home directory. Whereas in Fennel, macros are sandboxed, so it’s safe for static analysis to expand them, and the language server can understand where system comes from.
So I would guess that either Elixir macros are like Fennel’s (safe to run without worrying about side-effects) or elixir-lsp has a serious security flaw in it where simply by visiting a file in a potentially untrusted repository you are exposed to arbitrary code execution. Hopefully it’s the first one!
Edit: unfortunately upon further investigation it looks like it’s the second one! If you use elixir-ls you should probably uninstall it unless you can somehow guarantee you’ll never accidentally run it on an untrusted file. (you can’t)
or elixir-lsp has a serious security flaw in it where simply by visiting a file in a potentially untrusted repository you are exposed to arbitrary code execution.
I think there’s the same issue in Rust for the record. At least 2 years ago it was the case already and even if there’s a flag at rust-analyzer initialization to allow or disallow macro expansion, I think a lot of “out of the box” configurations tend to enable them by default rather than the opposite.
I didn’t know there was a way to expand macros in a sandbox that would still give out enough information for static analysis, but I think this is something that’s really easily overlooked today in the “ease of use / security” balance around language tooling these days, and there’s probably a good article to make to evangelize these things.
Ah, I guess @jec probably meant the first option, which makes sense. Thanks.
Whenever people talk about IDEs, I always think I must be missing out: I’ve never really used them much and so I’ve never really understood what it is that they do. I guess I’m like Paul Graham’s Blub programmer, unable to imagine the productivity gains offered by IDEs because I never do the things that they offer.
In the last few years, I started using LSP, so I now have some idea what that can do. I’m pretty sure I’ve seen colleagues do all that and more with Clojure in Emacs.
I find I very rarely do anything more exciting than go-to-definition though!
Yeah, go-to-definition is definitely the biggest win that you don’t get for free with the repl. It is very easy to implement on top of the repl tho: https://github.com/sanel/monroe/pull/15/files Honestly for any “IDE” function, it’s almost certain that Emacs has had support for it in some languages for multiple decades; it’s just that it might be a little fiddly to set up, and Java probably isn’t one of those languages. The idea that IDEs have features that aren’t available in text editors has always been wrong, but LSP makes it significantly more obviously wrong.
The benefit you get from on-the-fly compiler feedback is a lot more pronounced in a language with static types; trying to do OCaml without seeing the types in the editor is a big pain in the neck. But it is quite nice in Clojure to be able to see a little red underline instead of staring at a 5-page stack trace when you make a typo.
This might not be an easy question to answer in a forum comment but what is it that makes a framework “enterprise-grade”?
I don’t have a lot of experience with Spring, but I did just learn enough of it to be dangerous. Spring’s big idea is making software components something close to first-class. It does this by introducing an omnipresent dependency injection context.
For example, if I’m working on a UserService class to handle business logic relating to users, then I can wire a UserRepository class into it with a single annotation. An instance of UserRepository will be instantiated at runtime for me, and available for use by the time my UserService is called. The enterprise-y nature of Spring is the fact that it offers many libraries to use as turnkey solutions to common configurations, and how it promotes modular design from the get-go. Indeed, my example fell into a common Spring trope: splitting the business rules (UserService) away from the persistence logic (UserRepository) for reasons of testability and decoupling of concerns.
As an outsider looking in, I’ve never really understood what something like Spring Boot is actually for.
Anything you’d want a backend for, essentially.
Spring MVC is the first web framework I have actually enjoyed using. In a REST HTTP handler, I can declare that it takes a Query object, and returns a list of Results. The JSON serialization/deserialization occurs automatically based on the types. (I can configure it if need be.) If I happen to need the currently authenticated user, I add that to the function parameter list with a special annotation, and Spring injects it. Almost every other web framework/library makes me write all this gunky web code involving serialization to/from JSON, form parsing, and other incidental complexity imposed by the web.
That’s the good. On the bad, DI wiring failures are a runtime issue which can be frustrating. Startup times can be pretty slow. And there is a horrific amount of cargo-culted bad advice online, or good advice for old versions. You get the feeling that lots of people just throw stuff at the wall until it sort of works, then move on. Spring Security in particular is poorly documented if your use case falls far outside the defaults.
Overall, Spring Boot 3 and Kotlin is my current favorite backend stack. The maturity of most of the Spring libraries is superb. It really makes other ecosystems look decidedly more amateur by comparison. And enterprise is sometimes just a less hip way to say “scales.”
Thank you for taking the time to write all that: I wasn’t sure that I’d get an answer, let alone such a detailed one.
I once - about ten years ago - had to do some maintenance on a Java web app that used Spring. I didn’t do anywhere near enough work to really understand it but I do remember it being incredibly difficult to work out what code was actually running when. On top of the usual Java OO problems - everything was an interface but which implementation of the interface was actually being used? - there was this giant XML file that controlled how all the components were wired together. It put me off the idea of “dependency injection” for a long time.
Thank you for explaining the rationale in a way that actually makes some sense!
Yeah, this is a good summary. Particularly DI as runtime configuration is pretty important for handover to an SRE team that has to support many applications across multiple sites.
And enterprise is sometimes just a less hip way to say “scales.”
Yes, and “it scales” doesn’t simply mean it can handle lots of load, it also means it is accessible to a large rotating cast of developers.
To @c--, these days Spring’s DI config files are typically written as YAML rather than XML.
I don’t have a lot of experience of either modern IDEs or Clojure but I’m surprised to hear that. I thought REPL integrations were one of the killer features of Lisps: isn’t an editor with a REPL integration like a super-IDE?
The problem is that the author is trying to use an IDE, quite frankly. You get more mileage out of Emacs, and probably these days, even Vim. I’m not sure how things like Cursive really compare – might be acceptable. But, certainly you’re not getting a great experience without something extra.
I was an Emacs zealot for a long time, but I’ve grown out of it as I gained experience working in larger teams with people having broad variance of skill & experience. I’m quite comfortable with the circa-1980’s UX, but while getting an average Python or Java programmer to learn Clojure-the-language is not too difficult, asking them to learn Clojure-the-tooling is too much for many. And if they’re already accustomed to PyCharm or IntelliJ or VS Code it would be honestly irresponsible of me to insist they learn to use Emacs instead.
Yes, that’s the brunt of my original comment. Not only that, but I would not choose to start a new project for myself now using Clojure and I have 10+ years of it under my belt.
You probably mean that there’s no out-of-the-box, easy solution to just start using Clojure as you would any other language? I don’t know the last time you tried Clojure, but the days when your only choice was essentially Emacs+CIDER or Emacs+something-else are long gone. Today, Clojure has more (and I dare say better support than many other PLs) in a variety of editors/IDEs - Cursive in IntelliJ is great, Calva in VSCode is amazing, CIDER is still developing and continually improving. With nrepl enhancements of recent years and LSP, it really is an amazing and fun experience.
And don’t even get me started on Emacs. I use Emacs not because of what it can or cannot do, or what other editors or IDEs can do better. I use Emacs for what I can build on top of it. Like for example, does IntelliJ have good support for git worktrees? I don’t know, maybe it has absolutely mind-blowing superb support. But does it let you create a whole new worktree based on an existing GitHub issue or PR? Can your IDE let you search through your browser tabs or browser history and find whatever you need quickly? Can you copy&paste the URL of any tab in the browser in-place, while typing, without having to switch to the browser? I can git-blame URLs or get a full, complete URL to the specific line in code (on GitHub/Gitlab) without ever leaving Emacs. And there are tons of small things like that I do in Emacs every day.
And of course, I get to do all that in Lisp. Lisp is amazing and completely underrated. Structural editing alone allows you to write code like poetry. And the famous REPL-driven, interactive development is pure fun. Since switching to Lisp, I stopped feeling FOMO – I don’t feel like I’m missing out on not knowing a language or a platform. Whenever I want or have to write something for a specific platform, there’s always some Lisp dialect for it. Switching between CL, Clojure, Clojurescript, ClojureDart, Fennel, etc. is much easier than moving between Java/Golang/Python/etc.
I’m sorry, you have my sympathy for having to work in Java and having to debate how much boilerplate modern Java is able to reduce, but please don’t expect me to think that I’m the one missing out here. I’ve been having fun using Clojure as my main driver and the fun is not running out. There’s plenty of it. “Joy of Java” even sounds weird, like “bees against honey” or something.
I’ve no need for your sympathy, nor do I offer you pity. I don’t know you, and I expect nothing from you. You might have misinterpreted my comments above as some call to action, but I’m merely taking the space to explain why Clojure is no longer useful for me or my colleagues.
I will say as a 20-year user of Emacs: having it be promoted as the number one editor of choice for a language in 2023 is a deeply unserious position. Fine for individuals who have the ability and desire to hack their own tools, but I work with a team of 200+ developers, and it’s not an acceptable choice for any of them.
You can’t call Emacs “deeply unserious” and follow it up with suggesting not even one on two-hundred developers would find a better workflow than their current if they picked it up. Magit alone changed my life for the better.
200+ developers, and it’s not an acceptable choice for any of them.
Sounds more like “no choice” to me. In a world dominated by commercial, proprietary tooling, having a choice is vital. You may not acknowledge this today, but deep down you probably admit that your world is better partly because of people like me, users, supporters, and creators of Free and Open-source software, who are willing to make certain sacrifices for the sake of retaining choices.
I chose Emacs. Nobody forced me into it. Nobody offered me money for using it. No one ever advertised it to me. I chose it because I am 100%, absolutely sure that it can never be sold, acquired, embraced, and extinguished. It’s guaranteed to never ever have telemetry I can’t control. It will never show ads, useless pop-ups, or annoying banners. It will never have a subscription model or pricing structure. It won’t have a huge fragmentation problem. It will always have free and unlimited support from the community and will never steal or take my data hostage. Now, that is a choice. A deliberate, strategic, long-term survival mode decision.
I bet there are dozens of people among the 200+ you’ve mentioned who are willing to make a similar choice under the right circumstances. Generalizing is rarely factual. Saying shit like “Emacs is not a serious choice of editor…” is seriously disingenuous. Please don’t go that route. There are incredibly brilliant hackers among Emacsians and Lispers (including Clojurians too). Perhaps, let’s acknowledge that even if you disagree with their choices.
They certainly do have a choice of tooling, and they choose not Emacs, but the tooling that is familiar and commonly used by peers & colleagues. No other circumstances prevent them from choosing Emacs, except that it isn’t actually a viable option for 99% of them. Of my colleagues, I know of exactly 2 (myself included) who have chosen to invest time in making Emacs work for them. The rest simply want to get their own work done, and we have no problem budgeting licenses for them (including Cursive!)
There are incredibly brilliant hackers among Emacsians and Lispers (including Clojurians too). Perhaps, let’s acknowledge that even if you disagree with their choices.
https://clojure.org/news/2023/10/06/deref is the most recent description of the problem and their approach. If you want to nitty-gritty details, here’s done of the relevant tickets in their Jira demonstrating their current approach:
Clojure 1.12 alpha 6 was just released, which includes the method values changes. (This allows for writing, e.g. (map Integer/reverse (range 10)) without the anonymous function wrapper.) The functional interfaces will come probably by next month (once they’ve done bug fixes for this set of changes).
IMHO Java still hasn’t caught up to where Scala is, and will not do so for a long time yet. Scala supports SAM, a full-fledged implementation of pattern-matching, record types (case classes), and much more for a long time now.
From all I’ve seen, the ADTs are pretty poor. The pattern matching on the other hand seems pretty well done. I wish they would copy more from Scala though, but it’s probably not possible.
They actually are thinking about deconstructors, which would allow non-record classes to be part of pattern matching as well.
Why do you think that ADTs are pretty poor? Sealed interfaces may not be as ergonomic as in Haskell’s data Type = A | B notation, but they are a pretty smooth improvement over the existing inheritance/final class system. Records are pretty great in my opinion as well.
They are definitely a huge improvement. But compared to Scala, Haskell or even F#, they are still way behind. There are many problems with ergonomony and those are important, because bad ergonomy means people keep using plain primitive types like strings etc. In particular, is it still required to have one record class per file?
They also lack many abilities in terms of expressiveness, for example (to my knowledge) it’s impossible to model GADTs and it’s impossible to enforce state validation beyond what the typesystem offers both at runtime or compiletime (unless making the compiler throw an exception). Also, there is no concept of “cardinality” for Java’s records, so in the future it will be harder to build on top of them and distinguishing between records with different number of fields. Furthermore, Scala’s case-classes do have automatic copy-methods, which record classes are missing.
Haskell’s records have quite a few shortcomings in my opinion (though I’m sure there is some language extension for that as well), but that’s another topic :D
What do you mean by primitive types like Strings? You mean like rare use of stuff like simple wrapper types one would have with e.g. newtype in Haskell, that gives for a type-safe specific String, e.g. UserName?
one record per file
It was never a requirement - you can only have one public class per file, but you can even create a record as an inner class (it will automatically become a static inner class), or even inside a method body! I often make use of these in code bases.
I think you (as many others) under-value Java’s generics. They are definitely not as sophisticated as Scala’s or Haskell’s, but they are still very expressive - you can easily create all of the usual basic Haskell datatypes/interfaces (Monoid, recursively defined linked List, Applicative, etc.) It only breaks down just before Monads, but arguably, what 99% of people would ever use for, Java is capable of, type system wise.
E.g.
sealed interface List<T> permits Cons<T>, None<T> {
}
record Cons<T>(T head, List<T> tail) {}
record None<T>() {
static List<T> none() { // this is only required so that the generic type can be inferred at use-site
return new None<>();
}
}
I haven’t compiled this code, so it might not work as expected, but I have written it previously and it works. Not as nice as data List a = Cons a (List a) | None (my haskell is quite rusty, it might not be correct, also, they have a different naming convention I’m sure).
cardinality
Well, what would you use that for? Java is vehemently nominal in its typing system, and structural typing doesn’t fit well with it. For that, it can use reflection, I guess, which is quite okay with records.
What do you mean by primitive types like Strings? You mean like rare use of stuff like simple wrapper types one would have with e.g. newtype in Haskell, that gives for a type-safe specific String, e.g. UserName?
Yes that’s what I meant.
It was never a requirement - you can only have one public class per file, but you can even create a record as an inner class (it will automatically become a static inner class), or even inside a method body! I often make use of these in code bases.
Yeah well, that is a problem. This rule of enforcing one public member per file certainly had it’s advantages at some point. Nowadays it’s mostly a hindrance.
I think you (as many others) under-value Java’s generics.
If anything, it is the opposite! :-)
I work mainly in Scala. Scala’s creator is the one who originally introduced generics into Java, including doing their actual implementation. However, he wasn’t satisfied with Java, so he created Java.
It only breaks down just before Monads, but arguably, what 99% of people would ever use for, Java is capable of, type system wise.
Not at all. Even defining something as simple as a generel map-function (or filter, or …) on collections that returns the collection itself is impossible in Java. That alone already creates so much boilerplate. It also makes streams much harder to use. I’d say 99% of the java developer use one of the two above on a daily basis.
Well, what would you use that for? Java is vehemently nominal in its typing system, and structural typing doesn’t fit well with it. For that, it can use reflection, I guess, which is quite okay with records.
Yes and those are problems. In fact, Scalas typesystem is also heavily nonimal and in certain situations I much prefer typescripts structural approach.
These are in the works!
That’s great. No honestly! I couldn’t wish for anything more than Java just becoming Scala. That would make my life (and my choice of jobs) so much easier. But I’m afraid it will never happen. :-(
The error messages are terrible. I have no apologetics for this. ;-)
Even with the better error messages in Clojure 1.10, this is still a major issue and the core team has been largely disinterested in improving it. Runtime errors can’t be treated like other errors as they could be encountered from the host language (Java, in this case), but they could still be deliberately written and prepared for instead of relying on hard-to-decipher ClassCastExceptions, NullPointerExceptions, or similar.
To me, whatever they’ve done to the errors in the past few versions has been nothing but a serious regression… Maybe I’m biased because my brain was so used to decyphering the old ones.
Using spec to validate things like an ns form just doesn’t feel right. And it shows… those automatically generated errors are much more difficult to parse!
And I don’t remember what they did exactly to null pointer exception now, but I recall the new version is much harder to visually parse to me.
IMO, good error reporting is the one thing you shouldn’t automate away in a language… It takes a human touch, and you’ll have to use heuristics: It is hard work, and you don’t get to hammock-driven your way out of this!
Throwing spec/explain at your users so that they can decypher what they did wrong doesn’t show very high regard for people’s time.
It’s funny from developing my own language, I was like “yikes, these error messages suck” and then I remembered working in clojure & thought “well, I guess it could be worse”…
In serious note, it’s wild that at runtime where we have the most information we have horrible discipline & practices at actually displaying good messages. <- like what if we have a step that interprets the stack trace & deduces facts about what could be root cause.. and of course, catch null pointers & friends when root cause is obvious.
Feels like it shouldn’t be an impossible task, but I’ve yet to see really good runtime error messages.
I recently had a chance to start a greenfield project, after having supported a team using Clojure for applications development for over 10 years. I was surprised to find myself more productive in plain contemporary Java.
A decade ago, Clojure’s functional language features were a huge differentiation and worth taking a small hit in runtime performance in exchange for lightweight code. Java 6 was a painful exercise in verbose boilerplate programming.
And while Clojure is remarkably stable, and our software never “rotted” due to changes in the language, recent releases of Java have shown great progress in innovation which Clojure is slow to support.
A specific case is the “single abstract method interface” pattern that Java supports via lambda expressions. Important Java libraries, like the AWS SDK, use these types of interfaces a lot and they’re just awkward to program against using Clojure / Java interop. And this feature was introduced in Java 8, iirc which is now 10 years old!
10 years ago I had to evangelize and advocate for the choice of Clojure over other JVM languages, and I believe the last decade of productivity and business value has proven me correct. But I would find it hard to hold that position against the Java of today.
I have a hard time figuring out what makes good contemporary Java. My knowledge is mostly stuck in Java 5.
A big part of my gripe is boilerplate, and I don’t see that gone.
“single abstract method interface” seems interesting. I normally don’t bother with Java interop, preferring libraries that have solved this interop for me. Those libraries can be thin layers, but it’s still nice.
Meanwhile I’m still coming to grips with the performance penalty. There are ways to optimize Clojure, but I’ve not found them necessary… but am aware my code is less than the most performant.
Here are a few of my observations how Java has materially reduced boilerplate:
java.util.functionandjava.util.concurrent(since Java 5, but with many more useful APIs introduced later) packages use SAM types to make complex functional (and asynchronous) transformations easy and concise. Not as terse as Clojure’s->threading macro, but not terribly far apart either.varkeyword (since Java 10) for local type inference means you can get away with omitting type declarations pretty much everywhere except the function signature, yet still take advantage of Java’s strong static type system.recordclass declaration (since Java 16) for immutable datatypes.In practical use, these features let me write a submodule that deals with S3 via the AWS SDK in about 125 lines of Java. (All in, including imports, comments, logging, and all!) The Clojure version wasn’t appreciably shorter, but I also had to implement my own thin library layers to deal with the impedance of the AWS SDK.
Also
jshell(since Java 9) is an acceptable REPL.Edit to mention: Clojure’s IDE support absolutely sucks, and combined with the lack of enterprise-grade frameworks like Spring Boot, it’s still a really hard sell in a large company.
I don’t have a lot of experience of either modern IDEs or Clojure but I’m surprised to hear that. I thought REPL integrations were one of the killer features of Lisps: isn’t an editor with a REPL integration like a super-IDE?
This might not be an easy question to answer in a forum comment but what is it that makes a framework “enterprise-grade”? As an outsider looking in, I’ve never really understood what something like Spring Boot is actually for.
The problem is this statement could mean a number of things:
The last point is interesting, ‘cause I’ve found
elixir-lspto work very well with macros in general in Elixir, which is very macro heavy. I don’t know if it expands them, but it offers docs and autocompletion and highlights reasonably sensibly when a failure occurs in them and so on. Not familiar enough with either to really know whether they do something different though, or justelixir-lspfails gracefully enough I don’t really pay attention to it.I’d personally consider any macro that binds locals outside its own scope to be a huge footgun waiting to happen anyway, though that may be my Scheme/Rust showing through. Unless it’s the sort that’s literally generating boilerplate functions for you, I suppose.
I agree! I’m talking about macros which bind locals within their body. For instance, at work we have a testing macro that sets up a test-mode system and binds it; for instance:
This expands to a
letblock wheresystemis bound as a local inside the body.In Clojure, you can’t safely expand
with-systemin static analysis, because it could run arbitrary side-effects, including erasing your home directory. Whereas in Fennel, macros are sandboxed, so it’s safe for static analysis to expand them, and the language server can understand wheresystemcomes from.So I would guess that either Elixir macros are like Fennel’s (safe to run without worrying about side-effects) or
elixir-lsphas a serious security flaw in it where simply by visiting a file in a potentially untrusted repository you are exposed to arbitrary code execution. Hopefully it’s the first one!Edit: unfortunately upon further investigation it looks like it’s the second one! If you use
elixir-lsyou should probably uninstall it unless you can somehow guarantee you’ll never accidentally run it on an untrusted file. (you can’t)I think there’s the same issue in Rust for the record. At least 2 years ago it was the case already and even if there’s a flag at rust-analyzer initialization to allow or disallow macro expansion, I think a lot of “out of the box” configurations tend to enable them by default rather than the opposite.
I didn’t know there was a way to expand macros in a sandbox that would still give out enough information for static analysis, but I think this is something that’s really easily overlooked today in the “ease of use / security” balance around language tooling these days, and there’s probably a good article to make to evangelize these things.
Ah, I guess @jec probably meant the first option, which makes sense. Thanks.
Whenever people talk about IDEs, I always think I must be missing out: I’ve never really used them much and so I’ve never really understood what it is that they do. I guess I’m like Paul Graham’s Blub programmer, unable to imagine the productivity gains offered by IDEs because I never do the things that they offer.
In the last few years, I started using LSP, so I now have some idea what that can do. I’m pretty sure I’ve seen colleagues do all that and more with Clojure in Emacs.
I find I very rarely do anything more exciting than go-to-definition though!
Yeah, go-to-definition is definitely the biggest win that you don’t get for free with the repl. It is very easy to implement on top of the repl tho: https://github.com/sanel/monroe/pull/15/files Honestly for any “IDE” function, it’s almost certain that Emacs has had support for it in some languages for multiple decades; it’s just that it might be a little fiddly to set up, and Java probably isn’t one of those languages. The idea that IDEs have features that aren’t available in text editors has always been wrong, but LSP makes it significantly more obviously wrong.
The benefit you get from on-the-fly compiler feedback is a lot more pronounced in a language with static types; trying to do OCaml without seeing the types in the editor is a big pain in the neck. But it is quite nice in Clojure to be able to see a little red underline instead of staring at a 5-page stack trace when you make a typo.
I don’t have a lot of experience with Spring, but I did just learn enough of it to be dangerous. Spring’s big idea is making software components something close to first-class. It does this by introducing an omnipresent dependency injection context.
For example, if I’m working on a UserService class to handle business logic relating to users, then I can wire a UserRepository class into it with a single annotation. An instance of UserRepository will be instantiated at runtime for me, and available for use by the time my UserService is called. The enterprise-y nature of Spring is the fact that it offers many libraries to use as turnkey solutions to common configurations, and how it promotes modular design from the get-go. Indeed, my example fell into a common Spring trope: splitting the business rules (UserService) away from the persistence logic (UserRepository) for reasons of testability and decoupling of concerns.
Anything you’d want a backend for, essentially.
Spring MVC is the first web framework I have actually enjoyed using. In a REST HTTP handler, I can declare that it takes a Query object, and returns a list of Results. The JSON serialization/deserialization occurs automatically based on the types. (I can configure it if need be.) If I happen to need the currently authenticated user, I add that to the function parameter list with a special annotation, and Spring injects it. Almost every other web framework/library makes me write all this gunky web code involving serialization to/from JSON, form parsing, and other incidental complexity imposed by the web.
That’s the good. On the bad, DI wiring failures are a runtime issue which can be frustrating. Startup times can be pretty slow. And there is a horrific amount of cargo-culted bad advice online, or good advice for old versions. You get the feeling that lots of people just throw stuff at the wall until it sort of works, then move on. Spring Security in particular is poorly documented if your use case falls far outside the defaults.
Overall, Spring Boot 3 and Kotlin is my current favorite backend stack. The maturity of most of the Spring libraries is superb. It really makes other ecosystems look decidedly more amateur by comparison. And enterprise is sometimes just a less hip way to say “scales.”
Thank you for taking the time to write all that: I wasn’t sure that I’d get an answer, let alone such a detailed one.
I once - about ten years ago - had to do some maintenance on a Java web app that used Spring. I didn’t do anywhere near enough work to really understand it but I do remember it being incredibly difficult to work out what code was actually running when. On top of the usual Java OO problems - everything was an interface but which implementation of the interface was actually being used? - there was this giant XML file that controlled how all the components were wired together. It put me off the idea of “dependency injection” for a long time.
Thank you for explaining the rationale in a way that actually makes some sense!
Yeah, this is a good summary. Particularly DI as runtime configuration is pretty important for handover to an SRE team that has to support many applications across multiple sites.
Yes, and “it scales” doesn’t simply mean it can handle lots of load, it also means it is accessible to a large rotating cast of developers.
To @c--, these days Spring’s DI config files are typically written as YAML rather than XML.
The problem is that the author is trying to use an IDE, quite frankly. You get more mileage out of Emacs, and probably these days, even Vim. I’m not sure how things like Cursive really compare – might be acceptable. But, certainly you’re not getting a great experience without something extra.
I was an Emacs zealot for a long time, but I’ve grown out of it as I gained experience working in larger teams with people having broad variance of skill & experience. I’m quite comfortable with the circa-1980’s UX, but while getting an average Python or Java programmer to learn Clojure-the-language is not too difficult, asking them to learn Clojure-the-tooling is too much for many. And if they’re already accustomed to PyCharm or IntelliJ or VS Code it would be honestly irresponsible of me to insist they learn to use Emacs instead.
But, to your above point, it might be irresponsible to insist they learn Clojure, anyway, when Java has closed a lot of the gaps.
Yes, that’s the brunt of my original comment. Not only that, but I would not choose to start a new project for myself now using Clojure and I have 10+ years of it under my belt.
You probably mean that there’s no out-of-the-box, easy solution to just start using Clojure as you would any other language? I don’t know the last time you tried Clojure, but the days when your only choice was essentially Emacs+CIDER or Emacs+something-else are long gone. Today, Clojure has more (and I dare say better support than many other PLs) in a variety of editors/IDEs - Cursive in IntelliJ is great, Calva in VSCode is amazing, CIDER is still developing and continually improving. With nrepl enhancements of recent years and LSP, it really is an amazing and fun experience.
And don’t even get me started on Emacs. I use Emacs not because of what it can or cannot do, or what other editors or IDEs can do better. I use Emacs for what I can build on top of it. Like for example, does IntelliJ have good support for git worktrees? I don’t know, maybe it has absolutely mind-blowing superb support. But does it let you create a whole new worktree based on an existing GitHub issue or PR? Can your IDE let you search through your browser tabs or browser history and find whatever you need quickly? Can you copy&paste the URL of any tab in the browser in-place, while typing, without having to switch to the browser? I can git-blame URLs or get a full, complete URL to the specific line in code (on GitHub/Gitlab) without ever leaving Emacs. And there are tons of small things like that I do in Emacs every day.
And of course, I get to do all that in Lisp. Lisp is amazing and completely underrated. Structural editing alone allows you to write code like poetry. And the famous REPL-driven, interactive development is pure fun. Since switching to Lisp, I stopped feeling FOMO – I don’t feel like I’m missing out on not knowing a language or a platform. Whenever I want or have to write something for a specific platform, there’s always some Lisp dialect for it. Switching between CL, Clojure, Clojurescript, ClojureDart, Fennel, etc. is much easier than moving between Java/Golang/Python/etc.
I’m sorry, you have my sympathy for having to work in Java and having to debate how much boilerplate modern Java is able to reduce, but please don’t expect me to think that I’m the one missing out here. I’ve been having fun using Clojure as my main driver and the fun is not running out. There’s plenty of it. “Joy of Java” even sounds weird, like “bees against honey” or something.
I’ve no need for your sympathy, nor do I offer you pity. I don’t know you, and I expect nothing from you. You might have misinterpreted my comments above as some call to action, but I’m merely taking the space to explain why Clojure is no longer useful for me or my colleagues.
I will say as a 20-year user of Emacs: having it be promoted as the number one editor of choice for a language in 2023 is a deeply unserious position. Fine for individuals who have the ability and desire to hack their own tools, but I work with a team of 200+ developers, and it’s not an acceptable choice for any of them.
You can’t call Emacs “deeply unserious” and follow it up with suggesting not even one on two-hundred developers would find a better workflow than their current if they picked it up. Magit alone changed my life for the better.
Sure I could, even if that’s not what I wrote. Still here you go: Emacs is not a serious choice of editor for more than 0.5% of programmers.
Sounds more like “no choice” to me. In a world dominated by commercial, proprietary tooling, having a choice is vital. You may not acknowledge this today, but deep down you probably admit that your world is better partly because of people like me, users, supporters, and creators of Free and Open-source software, who are willing to make certain sacrifices for the sake of retaining choices.
I chose Emacs. Nobody forced me into it. Nobody offered me money for using it. No one ever advertised it to me. I chose it because I am 100%, absolutely sure that it can never be sold, acquired, embraced, and extinguished. It’s guaranteed to never ever have telemetry I can’t control. It will never show ads, useless pop-ups, or annoying banners. It will never have a subscription model or pricing structure. It won’t have a huge fragmentation problem. It will always have free and unlimited support from the community and will never steal or take my data hostage. Now, that is a choice. A deliberate, strategic, long-term survival mode decision.
I bet there are dozens of people among the 200+ you’ve mentioned who are willing to make a similar choice under the right circumstances. Generalizing is rarely factual. Saying shit like “Emacs is not a serious choice of editor…” is seriously disingenuous. Please don’t go that route. There are incredibly brilliant hackers among Emacsians and Lispers (including Clojurians too). Perhaps, let’s acknowledge that even if you disagree with their choices.
They certainly do have a choice of tooling, and they choose not Emacs, but the tooling that is familiar and commonly used by peers & colleagues. No other circumstances prevent them from choosing Emacs, except that it isn’t actually a viable option for 99% of them. Of my colleagues, I know of exactly 2 (myself included) who have chosen to invest time in making Emacs work for them. The rest simply want to get their own work done, and we have no problem budgeting licenses for them (including Cursive!)
0.5% acknowleged
This is coming in the forthcoming Clojure 1.12, but I won’t argue that it might not be enough.
That would be a fantastic improvement! When I was last looking at this back in April I came across this post https://ask.clojure.org/index.php/767/integration-with-java-util-function-interfaces where Alex Miller replied, “It’s in our scope list for 1.12” but he also said, “we have not decided yet what to do.”
Has there been any public design or code for this though? I can’t find any reference to it in the dev changelogs for 1.12.
https://clojure.org/news/2023/10/06/deref is the most recent description of the problem and their approach. If you want to nitty-gritty details, here’s done of the relevant tickets in their Jira demonstrating their current approach:
https://clojure.atlassian.net/browse/CLJ-2799
https://clojure.atlassian.net/browse/CLJ-2793
Clojure 1.12 alpha 6 was just released, which includes the method values changes. (This allows for writing, e.g.
(map Integer/reverse (range 10))without the anonymous function wrapper.) The functional interfaces will come probably by next month (once they’ve done bug fixes for this set of changes).IMHO Java still hasn’t caught up to where Scala is, and will not do so for a long time yet. Scala supports SAM, a full-fledged implementation of pattern-matching, record types (case classes), and much more for a long time now.
Yep, Scala is still miles ahead of Java and it will stay like that unless Java is willing to sacrifice backwards compatibility.
Java has algebraic data types (records, sealed interfaces/classes), and has basic support for pattern matching (more is coming).
It doesn’t want to be Scala, they occupy different niches.
From all I’ve seen, the ADTs are pretty poor. The pattern matching on the other hand seems pretty well done. I wish they would copy more from Scala though, but it’s probably not possible.
They actually are thinking about deconstructors, which would allow non-record classes to be part of pattern matching as well.
Why do you think that ADTs are pretty poor? Sealed interfaces may not be as ergonomic as in Haskell’s
data Type = A | Bnotation, but they are a pretty smooth improvement over the existing inheritance/final class system. Records are pretty great in my opinion as well.They are definitely a huge improvement. But compared to Scala, Haskell or even F#, they are still way behind. There are many problems with ergonomony and those are important, because bad ergonomy means people keep using plain primitive types like strings etc. In particular, is it still required to have one record class per file?
They also lack many abilities in terms of expressiveness, for example (to my knowledge) it’s impossible to model GADTs and it’s impossible to enforce state validation beyond what the typesystem offers both at runtime or compiletime (unless making the compiler throw an exception). Also, there is no concept of “cardinality” for Java’s records, so in the future it will be harder to build on top of them and distinguishing between records with different number of fields. Furthermore, Scala’s case-classes do have automatic copy-methods, which record classes are missing.
Haskell’s records have quite a few shortcomings in my opinion (though I’m sure there is some language extension for that as well), but that’s another topic :D
What do you mean by primitive types like Strings? You mean like rare use of stuff like simple wrapper types one would have with e.g.
newtypein Haskell, that gives for a type-safe specific String, e.g. UserName?It was never a requirement - you can only have one public class per file, but you can even create a record as an inner class (it will automatically become a static inner class), or even inside a method body! I often make use of these in code bases.
I think you (as many others) under-value Java’s generics. They are definitely not as sophisticated as Scala’s or Haskell’s, but they are still very expressive - you can easily create all of the usual basic Haskell datatypes/interfaces (Monoid, recursively defined linked List, Applicative, etc.) It only breaks down just before Monads, but arguably, what 99% of people would ever use for, Java is capable of, type system wise.
E.g.
I haven’t compiled this code, so it might not work as expected, but I have written it previously and it works. Not as nice as
data List a = Cons a (List a) | None(my haskell is quite rusty, it might not be correct, also, they have a different naming convention I’m sure).Well, what would you use that for? Java is vehemently nominal in its typing system, and structural typing doesn’t fit well with it. For that, it can use reflection, I guess, which is quite okay with records.
These are in the works! Or at least Brian Goetz has some thoughts about it, see https://github.com/openjdk/amber-docs/blob/master/eg-drafts/reconstruction-records-and-classes.md
My comparison was with Scala, not with Haskell.
Yes that’s what I meant.
Yeah well, that is a problem. This rule of enforcing one public member per file certainly had it’s advantages at some point. Nowadays it’s mostly a hindrance.
If anything, it is the opposite! :-) I work mainly in Scala. Scala’s creator is the one who originally introduced generics into Java, including doing their actual implementation. However, he wasn’t satisfied with Java, so he created Java.
Not at all. Even defining something as simple as a generel map-function (or filter, or …) on collections that returns the collection itself is impossible in Java. That alone already creates so much boilerplate. It also makes streams much harder to use. I’d say 99% of the java developer use one of the two above on a daily basis.
Yes and those are problems. In fact, Scalas typesystem is also heavily nonimal and in certain situations I much prefer typescripts structural approach.
That’s great. No honestly! I couldn’t wish for anything more than Java just becoming Scala. That would make my life (and my choice of jobs) so much easier. But I’m afraid it will never happen. :-(
It boggles the mind that I had to write this instead of just passing
fns around to Java methods expecting SAMs.Even with the better error messages in Clojure 1.10, this is still a major issue and the core team has been largely disinterested in improving it. Runtime errors can’t be treated like other errors as they could be encountered from the host language (Java, in this case), but they could still be deliberately written and prepared for instead of relying on hard-to-decipher ClassCastExceptions, NullPointerExceptions, or similar.
To me, whatever they’ve done to the errors in the past few versions has been nothing but a serious regression… Maybe I’m biased because my brain was so used to decyphering the old ones.
Using spec to validate things like an
nsform just doesn’t feel right. And it shows… those automatically generated errors are much more difficult to parse!And I don’t remember what they did exactly to null pointer exception now, but I recall the new version is much harder to visually parse to me.
IMO, good error reporting is the one thing you shouldn’t automate away in a language… It takes a human touch, and you’ll have to use heuristics: It is hard work, and you don’t get to hammock-driven your way out of this!
Throwing spec/explain at your users so that they can decypher what they did wrong doesn’t show very high regard for people’s time.
It’s funny from developing my own language, I was like “yikes, these error messages suck” and then I remembered working in clojure & thought “well, I guess it could be worse”…
In serious note, it’s wild that at runtime where we have the most information we have horrible discipline & practices at actually displaying good messages. <- like what if we have a step that interprets the stack trace & deduces facts about what could be root cause.. and of course, catch null pointers & friends when root cause is obvious.
Feels like it shouldn’t be an impossible task, but I’ve yet to see really good runtime error messages.
Not a LISP fan personally, but I do like EDN for having just enough data types to give some guarantees that JSON lacks. I wish it had better uptake.