Hacker News new | ask | show | jobs
by ragnese 1899 days ago
It surprises me as well. And to belabor my own point some more: there's a reason the #1 question about Rust from newbies seems to be "Is there a language like Rust, but with a garbage collector?" - sometimes reworded as "Is there a way to turn off the borrow checker?"

To be fair, I think that a decent amount is possible on JVM and CLR. Scala, for as much hate as it gets, has a much stronger type system than Kotlin/Java.

And Swift has its own backwards baggage! It has to work with Objective-C. There's actually a few weird things in Swift that I've bumped into that I'm pretty sure are only there because of Objective-C.

The Rust devs certainly take backwards compatibility very seriously and they guarantee backwards compatibility forever. There will be no breaking changes in Rust except in extreme cases of finding unsoundness or whatever.

There are currently ZERO good high level app-building languages, IMO.

Rust isn't it because of the lack of garbage collection (which is not a point against Rust- just for the "domain" of optimizing for user-space app development).

Swift would probably be it, but it's pretty much Apple-only and I don't expect that to change.

TypeScript is close, but it's held back by needing to work with JavaScript and the JavaScript standard library and ecosystem suck. It also can't do threads.

Kotlin/Java are decent, but not good at concurrency, and having things like different sized integer types is really silly for a high-level language where everything goes on the heap anyway...

Python is slow and can't do threads.

Some languages like Lisps and MLs might be good for building apps if they just had the ecosystem around it. Haskell would be a giant pain the ass because real apps are full of "IO".

So, yeah. It's kind of a miracle that we can get anything done. I guess that's why we get paid so well...

2 comments

Kotlin is the application language you're looking for (and Scala 3 to a lesser extent). Contrary to what you say it is the best language I've ever used for concurrency. It has it all, structured concurrency, cancelation, Flow, transparency (no await), etc. Regarding your second point the JVM is increasingly using the stack and with the soon complete generics you'll be able to avoid the boxed versions of the primitive types (though they remain useful when you want identity)
> Contrary to what you say it is the best language I've ever used for concurrency. It has it all, structured concurrency, cancelation, Flow, transparency (no await), etc.

I disagree.

Have you ever tried to actually implement something non-trivial that takes advantage of structured concurrency with cancellation? It's pretty hard to do correctly. Can you really tell me off the top of your head what the difference is between `withContext(coroutineContext) {}` and `coroutineScope {}` from within a suspend function?

Coroutines use unchecked exceptions for control flow. Kotlin also uses unchecked exceptions for fatal and non-fatal error handling. Figuring out how all these things interplay when it comes to coroutines and suspend functions has some subtleties that, IMO, are very difficult to figure out from just documentation and blog posts.

Also, Kotlin's standard types are entirely unsafe to use concurrently. The fact that MutableList inherits from List means that a function that accepts a List parameter CANNOT assume that the list wont change while the function is executing. So if you write `if (list.isNotEmpty()) { doSomething(list.first()) }` - that's a race condition because the list can literally become empty between the if clause and the body.

"But, wait! You should have just been smart enough to make a copy of your List before sending it between threads/coroutines." Okay, great. Let's do full copies of potentially-large collections. Thank goodness Kotlin is so concurrency ready that the standard collection types are persistent .. colle..ctions... oh.

Kotlin's concurrency story is really not that awesome. Scala is better, but still not perfect. Clojure is better still. Rust is good. Elixir (or anything with some kind of actor framework, I guess) is good. Haskell is good.

But I agree, overall, that if I had to pick a best app language today, it's either Kotlin or Scala, or Swift if you're writing for Apple stuff. I'll admit that I have a glaring experience gap with .NET languages, so I can't honestly say anything about C# and F#.

> I'll admit that I have a glaring experience gap with .NET languages, so I can't honestly say anything about C# and F#.

C#/.NET comes with language-level mutexes (`lock`) and the .NET library has thread-safe generic collections (ConcurrentDictionary, ConcurrentBag) and true immutable collections (ImmutableArray, ImmutableList, ImmutableDictionary) with optimized copy operations (e.g. ImmutableList.Add is O(1), but ImmutableArray.Add is O(n)). It's a nice addition to the library with only a few warts.

That's good to hear.

Java/Kotlin also have mutexes- just not as language built-ins (well, it does have `synchronized`).

They also have a ConcurrentFoo set of collections as well. And actually an ImmutableMap (but I don't see ImmutableList, etc. Why?).

The "problem" is that they're opt-in.

I spent years writing multi-threaded C++. But I've become very spoiled with modern languages that make concurrency safe(r)-by-default, such as Rust, Clojure, and Elixir (I haven't actually used concurrent Haskell).

Honestly, it's not anywhere near as bad as it used to be. The ecosystem(s) have embraced immutability everywhere that it's possible, so you don't have to worry quite as much about accidents.

> but I don't see ImmutableList, etc. Why

.NET doesn't have an ImmutableList either (the ImmutableBag type is an unordered collection). This is because unlike with hash-tables you always need to lock the entire structure when mutating a List/Vector (with hashtables you only need to lock the specific bin/bucket).

Ah! Good point.
> Coroutines use unchecked exceptions for control flow

That's... horrible.

Why can't Kotlin do it the way C# does with heap-allocated state-machines?

Honestly, I truly don't know and I don't feel qualified to judge the merits of the two approaches. There could be performance or behavior implications that they just prioritized differently from how C# does it. Or maybe something to do with the Java underpinnings. Are C# promises/whatever cancellable?

But, as an "end user", the exception thing drives me absolutely nuts. It wouldn't be nearly as bad if Kotlin either didn't use exceptions for ALL error handling, or even if it had checked exceptions so that "normal" errors would be, mentally, separate from fatal errors and/or coroutine cancellation.

> Are C# promises/whatever cancellable?

Yes, but it's opt-in (it requires the author of the async method to add a `CancellationToken` parameter and to respect `IsCancellationRequested`).

> Kotlin/Java are decent, but not good at concurrency

Since when JVM languages aren’t good at concurrency?

Also, primitives are usually not heap-allocated, so I don’t see what’s the problem with it. It makes the JVM a beast when it comes to number crunching.

> Since when JVM languages aren’t good at concurrency?

Since it's trivially easy to accidentally mutate data across concurrent contexts (threads). You have to actively remember to reach for Mutexes. You also have do understand how `synchronized` works and remember to actually use it. If you use a class that someone else wrote, you have to dig into their code (if available) to make sure they made their class thread-safe.

> Also, primitives are usually not heap-allocated, so I don’t see what’s the problem with it. It makes the JVM a beast when it comes to number crunching.

You don't need a number crunching beast to write most application-level software. That's my point. It's great for number crunching that you have byte, short, int, long as separate primitives. You know what most applications actually want? A number that wont magically wrap around to negative-LARGE_NUMBER when you guess the maximum size wrong. Java has fixed-size arrays, too, for super-performance mode. But applications just use List<> that has no size limit that you have to guess. It should do that same for Integers.