3 ms·
> First of all, Java's generics are already superior to C#'s. That's... highly controversial to say the least. You didn't just lose the ability to overload Lis
by unscaled 4y ago
> First of all, Java's generics are already superior to C#'s.
That's... highly controversial to say the least. You didn't just lose the ability to overload List<String> and List<Integer>. You've also lost run-time type information and type-safety when doing reflection. And reflection-based metaprogramming cannot be ignored in Java (and to a lesser degree in Kotlin): So much is based on it!
It also makes interfacing with primitives (especially arrays of primitives) quite messy. And you've also lost some possibilities for JIT optimizations, but I'm not sure if C# does them anyway and how much they really matter - so that part may be minor indeed.
Allowing different variance strategies could be a boon for a language-agonstic runtime, but, as other comments mentioned, that's just rewriting history. The JVM wasn't built to be a language-agnostic runtime. The truth thoroughly documented. It started with a new Java-like language, Pizza, that had to run on the then-current JVM, and continued with a fork of Java called Generic Java. These attempts were independent at first, so they could not change the JVM. Type erasure was chosen because it was the best fit to the existing Java model and had the best performance with the JVM as-it-was[1].
In hindsight it turned out to make writing JVM languages with different variance strategies easier, but it's not a superior design, just a coincidence. Although Martin Odersky was behind both Pizza and Scala, he did not mention variance as concern back in the 1990s when he chose type erasure. Let's not try to rationalize a JVM-compatibility decision that was done by a team outside Sun as some great insight by "the brilliant engineers at Sun".
In the end of the day, you can do type erasure just as well on .Net when you need it, the same way you'd do it in Java. Nothing prevents a CLR language from compiling List<T> as List<Object>.
> We've also added virtual threads years after C# had async/await, and now they're the ones stuck with that inferior solution, which we'll remain in the ecosystem even if and when they add user-mode threads.
Virtual threads are not inherently superior or inferior to the async/await mode. It's the old stackful coroutine vs. stackless coroutine or implicit-vs-explicit concurrency debate. I've already written about it multiple times in the past[2]:
Stackful pros:
* No "colored functions": I can write asynchronous I/O code the same way I write multi-threaded code or non-blocking code. It's definitely more ergonomic, but also means your code can hold more surprises for you. Not everybody thinks 'Colored Functions' are an absolute advantage, otherwise we wouldn't have Effect Typing.
* Less GC allocations: stack is [re-]allocated in bulk every time time a lightweight starts (go statement) or whenever it needs to grow. Stackless coroutines typically allocate a smaller object on every call to an async function.
Stackless pros:
* Clear yield points: you always know the points where your coroutine could suspend and control would be moved to another coroutine: whenever you see an await statement. This is why function colors are needed - to be explicit about the control flow.
* Less memory waste and less overall allocation size: the "compile-time stacks" generated by stackless coroutines are perfectly efficient. There is no unnecessary memory allocated.
I personally have a strong preference for explicitly marking concurrency, just like I would love to distinguish pure function and functions that have outside effects: I think Effect Typing is generally a good thing. Java also went its own way with explicit and mandatory effect marking when it comes to checked exceptions. Unfortunately, it never really got composability right, and using checked exceptions is simply too verbose - but Rust also went for explicit error type signatures, and its solution is generally well-received.
[1]: https://dl.acm.org/doi/abs/10.5555/647373.724066 https://dl.acm.org/doi/abs/10.5555/647373.724066
[2]: https://news.ycombinator.com/item?id=24361590 https://news.ycombinator.com/item?id=24361590
- pron 4y ago> And reflection-based metaprogramming cannot be ignored in Java (and to a lesser degree in Kotlin): So much is based on it! Its prevalence has been going down, and we're trying to encourage that trend. > Allowing different variance strategies could be a boon for a language-agonstic runtime, but, as other comments mentioned, that's just rewriting history. I don't see how this matters, and I never claimed that supporting, say, Clojure was the intent, but the design was very specifically to allow for multiple variance strategies (because Java 1.4 and Java 5 were two languages with different variance strategies). > Let's not try to rationalize a JVM-compatibility decision that was done by a team outside Sun as some great insight by "the brilliant engineers at Sun". I don't think I was doing that, but on the other hand let's not discount three decades and hundreds of decisions that have made and kept Java one of the biggest and longest-lived successes in software history. Call it lucky if you want, but they/we have been consistently lucky for three decades now. During that time, MS have made huge breaking changes to their platform at least three times. > Nothing prevents a CLR language from compiling List<T> as List<Object>. And Java could specialise just as Ceylon did. Erasure ended up working so well for Java because the main language on the platform did it. It is that, not some hypothetical ability, that was able to support Clojure and Kotlin well. > Virtual threads are not inherently superior or inferior to the async/await mode. It's the old stackful coroutine vs. stackless coroutine or implicit-vs-explicit concurrency debate. Actually, it isn't. User-mode threads might not be superior to syntactic stalkless coroutines in every language and on every platform (and certainly not for every use-case), but they are in Java (and could be in .NET). For one, we are now close to achieving zero memory waste in virtual threads compared to async (will be released in a year or so); in fact, we already allocate fewer objects than Kotlin's stalkless coroutines [1]. For another, while it is true that the less composable cooperative model (i.e. "clear scheduling points") has some advantages in languages like JS (where a non-cooperative model would break pretty much all existing code), Java and C# already have the non-cooperative model built into the language. It's a question of having two semantically similar yet syntactically incompatible concurrency models or just one. > Not everybody thinks 'Colored Functions' are an absolute advantage, otherwise we wouldn't have Effect Typing. I'm not a big believer in effect systems in general, for the simple reason that they've yet to show significant tangible bottom-line benefits (although we'll wait and see). But they're clearly wrong for coroutines in languages that already allow side-effects, like Java. The semantics -- in the precise Hoare-triplet sense -- are identical to regular mutation, and they're less composable. So they introduce an additional incompatible syntactic world with identical semantics, which makes them, at best, a redundant overhead. > Unfortunately, it never really got composability right, and using checked exceptions is simply too verbose You are absolutely correct there, but I believe we can fix that, and quite elegantly. Now, I don't want to give the impression that I think Java is a perfect language or platform; I don't. Mistakes were made along the way, especially early on. But I do think we've managed to do better than others. Python is as popular as Java or perhaps even more so, but it didn't manage to preserve compatibility; .NET never achieved Java's popularity while also failing to preserve compatibility (and several times). The only other languages with a similar backward compatibility story alongside long-lived super-popularity are C and JS, and both of which are famous for having a small or nonexistent standard library. So things could have hypothetically been better, but to date no one has actually managed to do that better, or even as well. [1]: The perfect memory efficiency of stalkless coroutines is only achievable when you disallow virtual calls and recursion -- as Rust does, although it pays for that dearly -- but neither C# nor Kotlin can or want to do that.