Hacker News new | ask | show | jobs
Java is becoming more like Rust, and I am here for it (joshaustin.tech)
80 points by joshaustintech 843 days ago
18 comments

What "immutable" means in Rust is very different from Java or any FP language for thar matter.

In Rust you can take any data structure and mutate it. There are hardly any invariants that you can force, unlike in Java. For example, strings are mutable in Rust. This, coupled with not being a language managed by GC, makes persistent data structures not practical or desirable.

Of course, mutability in Rust is very controlled, as variables being mutated can't be observed in an unsafe way. So it matters less if strings are mutable. E.g., you won't be able to mutate a string while it's used as a key in a hashmap.

But the fact is that, while Rust provides a high degree of safety, functions in Rust are usually side effecting, which means no referential transparency, no equational reasoning, hard to refactor, etc.

In Java, culturally speaking, you don't see much immutability, but when you have immutability, it tends to matter much like in FP languages. And Rust is definitely not one of those languages.

Mutation in Rust is much more controlled than in SML or OCaml. Any record or variable (sic!) can contain a ref cell, or a special data type like an array, which can be mutated regardless of the type of the thing it’s contained within. That’s the ML approach to mutation.

In Rust on the other hand, even though ref cells are a thing as well, the main approach to mutation is through mutable bindings and mutable references, which preserve const-correctness (inspired by C++), i.e. there is a certain degree of transitivity in the guarantees around immutability. When I see an immutable reference in Rust and pass it to a function (or a method), then, outside of few special cases, I can reasonably expect it to stay unmodified. In ML I can’t, unless the code is written in a very unoptimal way, i.e. with persistent data structures anywhere and everywhere, and still the type system won’t give me any hints about that.

> In Java, culturally speaking, you don't see much immutability, but when you have immutability

This isn't strictly true. Almost all Java code I see day to day are immutable classes outside of those being used with JPA.

> In Rust you can take any data structure and mutate it.

What if a memory location not marked "mut" is allocated by the compiler in the executable TEXT segment, so the virtual memory page is itself mapped read only?

Are you certain you can get away with what you said, within the promises of the compiler?

The underlying object type would still be mutable in this case, you just can't get a &mut reference to the object.
You get a segmentation violation if you try writing to a read-only page as in variables in a .text segment.

Are we talking about different things? I'm writing Rust too (and decades of C) and feel I'm missing what you're describing, perhaps.

The examples are good but I am not really a fan of the title. It's not really Java becoming more Rust like, Rust didn't invent these concepts. I talked to dl about some of these concepts going into Java a decade ago because of Scala. In PL a lot of concepts have been around for a very long time. Java is becoming more type safe, or more functional would be better titles I think. But the comparison code between Rust and Java 21 is good.
>It's not really Java becoming more Rust like, Rust didn't invent these concepts.

I see what you're saying, and definitely agree in a way, but also those two things aren't incompatible.

Rust didn't invent those concepts, but Java is becoming more like it by incorporating them. If Rust is much more widely known about than other languages that have those things, it may be more informative to people to hear "Java is getting some stuff from Rust" than to hear a statement that apportions the credit in more detail.

The article is using immutable records and sum types as “more like Rust” and I am just sitting in the corner, wondering whether everyone has forgotten that these things came from FP in the first place. In my mind, a language isn’t “Rust-like” because it uses concepts we learned in FP.

We have languages like Haskell and, before it, ML. And I know ML didn’t invent it. We also have Scala, and for Scala, we don’t even have to leave the JVM. There’s also F#, which is a kind of cousin-of-a-cousin to Java.

Rust has sum types and immutability because tons of people thought these features were important, and lots of languages adopted them in parallel with the appearance of Rust.

> Disclaimer: In no way am I claiming that these Rust features caused their counterparts to appear in Java

> I would not be surprised if Kotlin and/or Scala were more influential in bringing this to life.

But yeah, I agree, generally weak article. I don't think it shows that "Java is becoming more like Rust" at all, because of the two listed features.

A better way to say it is that Java is becoming more like Kotlin, which in turn was halfway between Java and Scala.
“Java is becoming more like Kotlin” lands you on the lower half of front page at best. “More like Rust”? Instant upvotes, #2 right now.
Except its not becoming more like Kotlin. It's becoming more like ML and OCaml
Very interesting to see concepts from ML/Haskell/Scala and other FP languages being attributed to Rust. If you tell me that a language is “Rust-like,” my mind goes to borrow checking and memory safety, not FP, so I opened this expecting to see some memory safety features added to JNI or something like that.
Everyone hasn't forgotten, they just aren't learning things from the bottom-up. It's Rust-like to the author because that's probably where he learned about it in the first place.
> It's Rust-like to the author because that's probably where he learned

I like about HN that everyone is so friendly about this type of thing. I personally don’t have a lot of patience for people who think new things are the greatest inventions and ‘the old is catching up’ (in this case Java catching up with Rust) while everything came from elsewhere originally. If you want to openly say these things, please research them first and attribute properly. But indeed it is good here we try to educate not shame or insult like some other communities.

Anyway: I would say people should learn bottom-up as we don’t need to invent the wheel every few years that way.

I feel the same lack of patience often, but in the end I respect anyone for putting themselves and their knowledge out there for critique.
This was my knee-jerk too. It’s great to see some of the good bits of ML flow into new languages, but this isn’t some Rust-specific feature. As an annoying side-effect tho, it’s been harder to pitch FP in these existing spaces since now “we have FP at home” in the existing language (even if the ergonomics are worse, it looks uglier, the language idioms & existing programs don’t match the style, & ’the bad parts’ + footguns still exist).
Hey, incremental improvements are still improvements. Convincing everyone to change the world is hard just for immutable types or sum types or something. But I’ve seen that slowly adding in functional programming to Java caused my team to get aggressive about using it, and demand it in code reviews. Sometimes you just need the Trojan horse and people will like what they find. I don’t think I’ll convince my team to drop Java, but they’re convinced that FP is important to include, at least in little bits.
Isn't F# more like a nephew of OCaml and cousin of C#?

F# "originated as an OCaml implementation", which runs on .NET with some C# interop support?

You can have more than one cousin. C# and Java are cousins, obviously, I don’t think I need to explain that one. F# and C# are also cousins, naturally.
They didn't come from FP either. Records and variant records were common features of imperative languages (Algol, Pascal, Ada, Modula). They were considered to be subsumed by classes when OOP became popular.

I believe FP did give us the pattern matching syntax for destructuring variants though.

The Java stuff is definitely coming from FP and not Rust at all.
Haskell + Ada + '{}' == Rust
It's consistently been impressive to me how thoughtful the Java language is being steered. Especially the explicit choice to NOT have an async keyword where other languages were all falling over themselves to add it, with all the complexity it entails (and in Rust it's a pretty awkward fit).

Once projects Valhalla and Panama land Java will be transformed again. Exciting times.

I think async worked out pretty well with JavaScript. But that was added to an already async language, it was just verbose and awkward before the await keyword.
In what way was the language async? It has no threads outside workers, which have strict limits regarding data sharing. You would see a lot of code that registers callbacks as event handlers, but I don't think that makes the language async. That's just the nature of event handlers. They are invoked later, when the event happens.

In a way, the lack of threading allowed `async` to be implemented without gotchas. In languages with first-class thread support, `async` implementations can run into thread issues. (Will the continuation run on the same thread? Does the code care?)

Async and multithreading are not the same thing. Async is about non-blocking execution between functions.

JavaScript is inherently async because the concept of the event loop is baked into the very language runtime itself.

In Java you can achieve a single threaded async runtime by instantiating a SingleThreadExecutor and having all your application code running as jobs posted to this executor. But because you can opt out of this model or use something like a ThreadPoolExecutor it's harder to bake in an `async` keyword into the language that is as simple as you have in JavaScript.

I know they're not the same thing. But first class thread support can make asynchrony harder to implement since it sometimes matters which thread the continuation runs on.

> JavaScript is inherently async because the concept of the event loop is baked into the very language runtime itself.

Ok, I hadn't thought about it this way. Early UI frameworks for java had this property as well. I don't know about the implementation. But there was some kind of message loop running, and you'd register event handlers, and events would get dispatched to your handlers.

You could argue that was the framework, not the language. Maybe I'm wrong, but I think of the message loop as being a property of the execution environment (DOM/node) rather than the language. Maybe that's not a real distinction though.

Sounds like parent was using async to mean that JS has single-threaded concurrent functions by default.

Disclaimer: I'm just the messenger here so you might hate what follows, the community is choosing the words.

Concurrency as a keyword is increasingly being used to differentiate single-threaded event handling from parallel processing (threads). As long as event handling is quick enough, it can give the illusion of parallel processing. And in some cases it can outperform parallel processing because the cost of context switching between events can be lower than context switching between threads. See https://medium.com/@caophuc799/nginx-architecture-and-why-ca...

The async keyword, in practice, simplifies the callback boilerplate required to handle the event model. When used in communication, async is increasingly being used as a word to describe "Single-threaded Concurrent Functions Built On Hidden Event Handling", because there is no succinct way of saying STCFBOHEH in English.

So: JS has async/STCFBOHEH/context-switching on its functions by default whereas Java's functions are (by default) executed in order without async/STCFBOHEH/context-switching.

I'm not an expert or anything, but I've never seen an implementation of event handling that was anything other than STCFBOHEH. (hadn't heard the term)

My understanding of "async" as a language feature is basically support for an `await` keyword. Having a message loop and event dispatcher seems like a less useful definition, but good to know, I guess.

I think you have async mixed up with concurrency / parallelism. Async is all about events as opposed to blocking wait. As an example, Java had predominantly sync programming model, because the thread would be block-waiting for stuff, no matter the fact there can many such threads block-waiting.
I don't think so. Any language that had event handlers would be just as "async" as the OG javascript. First class thread support matters because sometimes it matters which thread the event/continuation runs on.
I imagine the lineage here is more Scala than Rust, right? Not that Scala invented these concepts, but if you’re looking for proven features with JVM implementations to add to Java, you’re probably going to look to Scala (and Kotlin) first.
The OpenJdk team has said multiple times that their inspirations are coming from ML and not Scala or Kotlin.
I’m sure that’s true to an extent, but calling your sum types “sealed” is pretty explicitly borrowing from Scala. Which is fine. I think it’s good that they’re pulling inspiration from a bunch of different places.
This seems hard to believe. If true, it seems very odd that they wouldn't be studying successful languages with enormous real world adoption targeting the same platform as theirs.
To quote Brian Goetz [0], the language architect:

> It is super-common to observe "I saw feature F in language X, then I saw it come to Java, they must have gotten their inspiration from X." And while sometimes this is true, it is far more common that both are drawing inspiration from a common source or literature, sometimes even one that predates either language. (It is also super-easy to confuse "Language X was the first place I saw feature F" with "X invented F.")

Another in reference to getting inspiration from OCaml [1]:

> This is exactly the reaction that I was describing by "might elicit strong reactions from some", which is what I would call a "who moved my cheese" objection. Good, now we've got it out of the way. I had the same concern for the first few seconds after seeing this idea (the inspiration came from work being done in OCaml), but it didn't take much longer to see how it fit into the bigger role of switch.

Another from Ron Pressler [2]:

> Obviously, we do borrow features from less popular languages (primarily ML, which was never popular), but only when we think they're the best way to address a real need of Java's millions of mainstream developers and not just because those languages' self-selecting users love them.

This has all been discussed multiple times on the /r/java subreddit.

[0] https://www.reddit.com/r/java/comments/11322uy/jep_draft_imp...

[1] https://www.reddit.com/r/java/comments/18hglp5/effect_cases_...

[2] https://www.reddit.com/r/java/comments/18ynhn1/which_kotlin_...

I'm a big Rust person but the language I think that's slept on is Kotlin.

It has all the goodies I like in Rust (multiplatform, decorators, FP, pattern matching) but with a GC/JVM to back it.

I personally like Rust since I can drop down to low-level embedded projects and stick with one language, but depending on what interest you, Kotlin can be an excellent fit.

Can’t be bothered with Kotlin as you have no choice but to use IntelliJ to work with it.
Does Java even have custom value types ? Looks like author is attributing FP features to Rust ?
Not yet, but that's one of the big impending projects.
I am here for it too. Java's release cycle is now 6 months (matching Go's), and Java is introducing more of the features that make the 'newer' languages pleasant to use. Features like Pattern Matching for switch[1], Virtual threads [2], and String templates[3]

[1] https://openjdk.org/jeps/441 [2] https://openjdk.org/jeps/444 [3] https://openjdk.org/jeps/430

Isn't this more like "Java is becoming more like C#" ... again?

Record types, immutable after construction: https://www.c-sharpcorner.com/article/c-sharp-9-0-introducti...

Warning when a switch isn't exhaustive: https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

Record types in Java were added with Java 14, released March 2020, it was later that year, in November, that C# added them with C# 9.0.

I don't believe C# has anything like the exhaustive switch over a sealed interface that the article is talking about.

> it was later that year, in November, that C# added them with C# 9.0.

OK, then I am going to regard this as "around the same time". Every new C# feature is released "in November", since .NET major releases drop in November every year. That's the release cadence.

But language design decisions of what to add in that release are not done inside that 1 release month, or in any 1 month. They are done over much longer time periods, often incrementally over successive years.

I'm surprised there aren't more people using Java nowadays. The JVM is rock solid, the speed is really good, and the latest changes are making the language much more modern.

Maybe people are actually using it but not talking about it? Not sure.

Who's not using it? Rhetorical, but Java is everywhere still, especially near anything that's making money. (As opposed to, perhaps, hobbyist solo projects, which if that's your only sample size perhaps it looks like Java isn't as popular?)

These days it's actually more interactive and dynamic to develop applications with than a lot of dynamic languages, thanks to the improvements over the years.

I have been playing with the zgc garbage collector and gc pauses are essentially a solved problem. On machines with 90+ gb of heap size gc pauses are taking 700 microseconds (us) at most.
Generic erasure inside the JVM is a fundamental flaw.
There are some (imperfect) workarounds, ex: https://github.com/jhalterman/typetools
Why do you say that?
Why do I want an unnecessary VM in the way, and no access to native APIs?
Most of these features were available in Scala and Clojure 5-10 years ago - even though OpenJDK claims that they took these features from ML, I'm glad they are catching up to others in the JVM ecosystem.
That's a pretty short list.
Last I tried, if I used a method on the object then it would get allocated. This is understandable, but it means that if you want to actually take advantage of SR you have to use a static method.
I think a better description is "Java is becoming more like C#", which got these immutable-by-default record types many years ago.
Actually records in Java were released a full 6 months before C#'s version of records.
C# still, to my regret, doesn't have anything like sealed interfaces. The closest thing I've found is an abstract class with an `internal` constructor. That prevents sub-classing from outside the compilation unit, but it still doesn't exactly capture the intent.
I don't like dead-cat-themed code examples.
Rust did not invent immutable by default. This appears to be some kind of blog spam. More than 50% of the "article" is just trivial code examples backing up a non-existent point. Perhaps the author is looking to get a job in Rust and is padding out their blog with "Praise Rust!" evangelizing.
Modern Java is great! Java "patterns" are overwhelming.
most languages are now copying from each other, so we might get a language-convergence one day?