“… it has the more ambitious agenda to heal the rift between primitives and objects.”
I have been waiting for these fixes to the language since I was first introduced to Rust which got the type system “correct” where Java had left a lot to be desired.
The big thing that Java will get beyond generics and such being cleaned up for all types, is true Optional types. That is, a stack based Optional that is itself not null, but tracks something that might be null in the interior.
It will take a long time to update the ecosystem to correct this, but when it comes it will make Java a much “safer” language to develop in, that is a class of runtime errors can be avoided and caught at compile time. This will be great, and I look forward to the day when it comes.
The main thing of Rust that Java can’t provide even after Valhalla is compile time thread safety guarantees. I find those in the meantime one of the biggest advantages of using Rust - and miss it in all other programming languages. Questions like „can I refactor this code to make it parallel und use the database client / IO stream / etc from multiple threads simply answer themselves.
Apartment from that I think the type system power of Java/Kotlin in the near future is good enough and a nice blend of complexity and getting things done. And with Valhalla, Loom and modern GCs a lot of performance concerns should also be removed
Rust can only guarantee that no data race will happen.
Other race conditions are still possible, so I don’t really see Rust that much safer than Java. Concurrency is hard everywhere.
I'm not sure reasoning about the correctness of those would be easier in Rust, but if there are any rust experts reading this I'd love your perspective.
Interesting - didn’t knew tsan is now available for Java. Which JDK version is it available for? The linked wiki isn’t super clear on this. But given it seems a 2020 Addition I guess only for rather recent ones.
While that is the only guarantee it can provide - in practice it ends up being a lot more powerful. Eg most multithreading issues in Java occur due to totally missing synchronization - because developers either didn’t think about threads or didn’t have enough experience to apply correct synchronization. In Rust the compiler would already have complained about these cases, and eg the design of the Mutex type - which wraps data instead of standing next to it - makes it hard to abuse.
Have you ever tried kotlin? I've found it to be an encoding of best practices from Java with nullability pinned down. Really made me enjoy working in a most OO/procedural language again after fleeing to scala for a few years.
I had a look at Kotlin and it's definitively an huge improvement with regards to Java. It is free of all the ceremony and boilerplate of Java and null values are so much less of an issue. Still, coming from Scala I'm a bit underwhelmed.
Don't get me wrong - Kotlin is a very fine language with lot of promise and the comparison to Scala is not exactly fair - Scala is just a completely different league.
I really like what Odersky and the Scala community did with Scala 3; they lifted the language onto a sound new theoretical framework, streamlined the way implicits are used and got rid of puzzling irregularities. Did I mention the new sound macro system? But I'll stop fawning now.
I'm currently working on a project where we use Kotlin instead of Java. Probably wouldn't have taken the job if it was with Java and Kotlin makes it quite fun. But damn - I do miss Scala.
No JVM language can solve problem of memory layout.
I don't care about nullability in my code (to be honest, I don't remember when I seen NullPointerException last time), but I need good performance. At $job we use "arrays of primitives" instead of "array of structs" to have good performance, but such code is very ugly and hard to support.
True "primitive" objects which are packed densely in memory and generics over such objects (to use ArrayList or equivalent instead of bare array[]) is main reason what allows .NET code to be faster than JVM code, though .Net compiler (JIT) and VM are much less optimized. But JVM (HotSpot) doesn't have choice now :-( Valhalla, when fully implemented, will gibe a huge and long-awaited performance boost.
All these hyped non-nullable Option<> maybe good, but is not a show-stopper. Performance is.
I second that. I don't see NPEs in real life outside of early stage development of a new service. You inject all dependencies in constructors (Preconditions.checkNotNull if you're paranoid enough), make all fields final, and that's about it. I have got very mixed feelings about the extend to which Kotlin goes on null safety for this reason.
In traditional/non-FP code bases it's not a problem at all to check for null when a data structure uses it to signify "missing". There are usually not that many places where it's necessary.
Getting Java on par with C++/STL would be great though. No need for trove4j/colt. Anything that reduces memory usage without making people think hard would also be great to compete with golang.
Scala 3 has an optional non-null compilation mode which will turn Null into a separate type not subset of every class. In my opinion it is a much more elegant solution than Kotlin’s.
It's exactly as elegant for nulls. Kotlin's T? == Scala 3's T | Null. In both Scala 3 and Kotlin, you can't call a method it.
Kotlin does have the !! operator for practical purposes like Java interop. Equivalent to this Scala 3 code:
extension (t: T)
def !!: T = if(t != null) t else throw NullPointerException()
I haven't checked after release, how is Scala 3 smart-casting now? E.g. Kotlin and Typescript convert a T? to a T inside an if(t != null) branch, and Scala's left much to be desired when I checked.
Scala's and Typescript's union & intersection + smartcasting approach is much more general than Kotlin's T? of course, but Kotlin could introduce that backwards-compatible if they wanted.
> "I haven't checked after release, how is Scala 3 smart-casting now? E.g. Kotlin and Typescript convert a T? to a T inside an if(t != null) branch, and Scala's left much to be desired when I checked."
Why would you convert the entire project to Kotlin? It’s trivial to mix the two languages, and in fact most Kotlin applications depend on Java libraries.
I don’t think that’s true. One of the biggest use cases for Kotlin is Android development. Interop between Kotlin, Java libraries, and the Android SDK is completely seamless.
It's hard to be certain but we have seen this story in Scala. Among the advantages advertised early was interoperability with Java and access to its huge ecosystem. In practice the Scala ecosystem is completely different (well, Akka is a special case popular in Java projects too).
As a trivial example people would rather use ScalaPB instead of officially supported Java library. When I worked on a Kotlin-based system people were adamant to use github.com/JetBrains/Exposed. Which had a couple pages of documentation and was missing features.
And then again there's politics. Big/successful companies always end up re-inventing PL wheels. The whole GOOG/ORCL mess gave GOOG incentive to fragment the Java ecosystem too.
Ok that’s Scala… Kotlin is already the primary language for all new Android applications. The interop story with Java is unambiguous: Kotlin works well with Java. For instance, Android didn’t rewrite most of their SDK in Kotlin, yet it works just fine. Popular Java libraries like Spring and Dagger work with Kotlin. It really “just works”.
Due to backwards compatibility Optionals will be value classes, not primitives so null will be a possible value for them as far as I know. But it will be possible to optimize it at many places (mostly as return types perhaps).
If I understand the Valhalla proposed design [0], every primitive reference type `X` will have an associated primitive value type `X.val`, while every primitive value type `X` will have an associated primitive reference type `X.ref`. Which one gets to be called `X` is a function of how you declare it -- `primitive class X.val {}` gives primacy to the reference type.
So, if `Optional` will continue to be a reference type, you should be able to use `Optional.val` to use the associated value type.
I believe JEP 401 has not yet been updated to the new model,
there is no .val anymore, instead there are nullable value types and non nullable primitive types, only the latter has a .ref box because the former is a reference.
Optional will be retrofitted to a value class not a primitive class,
you get flattening but only on stack (parameter type or return type) not on heap (field).
I don't really understand the end of the part3 (the VM model), so i may be wrong.
> Boxed ints have identity, whereas primitives do not; boxing is not able to fully paper over this gap.
I don't know why the glossed over small values of Integer's sharing identity (effectively behaving like values) when discussing the complexity of these things.
From 5.1.7. Boxing Conversion[0]: "If the value p being boxed is true, false, a byte, a char in the range \u0000 to \u007f, or an int or short number between -128 and 127, then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2."
It's such a shame that value classes in Valhalla can only be immutable...
C# has mutable value based classes since years and this enable major optimizations in much more common use cases. This is also what everybody do in C.
In addition to being less expressive and programmable, it should also be slower, e.g you have a value class that has ten fields, you just want to update one and ineptly, you have to reconstruct the whole object and reassign/copy every fields that have not changed..
Regarding ergonomics, they are totally broken, e.g you can't have an idiomatic Point.
You cannot write point.x = 5
Basically, p = p with { .x = 3 } or something like that. But that will also planned to work for other kind of classes and will be a basis for advanced pattern matching as well.
Also, in case of a primitive class with no guarantee of non-tearing, in practice you will get the same performance as with a mutable struct, maybe even better as the JVM can better reason about it.
Mutable value types in C# have a place, but they also come with plenty of pitfalls. Java tends to lean in a direction where those don't exist. Cf. captured variables in closures as well: having to treat them as final avoids a number of problems.
I have to say his writing style is very easy to follow. I tend to get distracted when reading technical text, but this was very easy to stay on track with
It took python what, over a decade to get people to switch from 2 to 3 didn’t it?
I’m just not sure you could ever pull it off. It seems way harder than that. Even with some sort of compatibility layer. I think you’d really end up in more of a perl 6 situation where it’s a different language that would eventually have to be renamed due to confusion.
They may not have a choice, because J1 has problems evolving and it takes a decade (or never) to make major improvements. Just leave it and support it for another 20 years on the side.
A lot of isolated Java could remain V1 no problem.
Allow new starts, separate modules, and those that want to port a J2.
Python 3 was a disaster because they never bothered to think about it, provided no facility for it, didn't care about collisions at the platform level.
Oracle could literally pay for the most important OSS modules to be ported, give architectural guidance scripts to transform old codebases, special test units, training.
Python and Javascript are ironically two of the biggest messed in all of tech, that gain popularity and drag everything into the mud with them.
"Java 2" is already out - it's Kotlin (or Scala if you're so inclined) ;)
Unfortunately the more compatible you are with "V1" the more compromises you have to make. See: Kotlin, F#, TypeScript bending over backwards to maintain compatibility with the dominant language of their respective platforms. Not to mention C++. You have to admire the hutzpah of Python 3 level of changes. Some software should simply have a "sell by" date and be thrown out when it starts to stink.
That said I'm surprised more languages haven't attempted the approach of Rust with 'editions'.
> There are times, however, when it is useful to be able to make small changes to the language that are not backwards compatible. The most obvious example is introducing a new keyword... Editions are the mechanism we use to solve this problem. When we want to release a feature that would otherwise be backwards incompatible, we do so as part of a new Rust edition.
As mentioned in the article, “A growable language” shows us that a programming language has to evolve. I think Java does an excellent job of not including everything and the kitchen sink right away, but wait a bit for the hype to die off and usually implements new features when they are worthy, often in a superior way. This “last mover advantage” was even part of the initial ideas behind Java.
The last movers advantage is a tradeoff. It becomes increasingly difficult to make major paradigm shifting changes the further you go. Being conversative increases the length of the runway and Java has benefited from that. Though, the superiority of Java's version when it finally gets some feature is often debatable.
Two major initiatives for many years in Java have been Loom and Valhalla. A cynical take on that is that Java is attempting to copy something that Go and C# (and others) have had and excelled at for a decade or more, and were designed to do from the start. Sometimes, at some point the expense and complexity of dragging decades of code forward (with all accumulated mistakes, design assumptions made based on now obsolete hardware, backwards compat constraints, etc.) outweighs the cost of going back to the drawing board to start again.
But Java attempts to “copy” those with an order of magnitude larger ecosystem, with the benefit of all the other optimizations/state of the art GCs that went into the OpenJDK project, and with true backwards compatibility of the billions of lines of code out there. I don’t think that reinventing the wheel would really put us forward. According to the ‘No silver bullets’ article, there is no significant (that is, order of magnitude) productivity improvement in language design since high level languages. The only such bullet is reusing existing libraries.
(Also, do note that we still rely heavily on numerical libraries written in fortran)
I agree the order of magnitude improvement is no more. Programming in C is much the same as Java. Both are high level curly brace languages. But there is room for improvement still, quite a bit. The fact that Kotlin, Go, etc. were created and became popular supports the fact that large numbers of folks believed that Java's design direction has run it's course to some degree. I may be proven wrong, Valhalla and Loom could be smashing successes and everyone will flock back to Java. But more likely it will take a decade to reap the benefits of the features Valhalla and Loom would provide, benefits already being reaped by other ecosystems right now.
State of the art GCs are incredible - but there is a reason you see Java pioneering the GC area, that being that Java has the most to gain because of its tendency to create enormous amounts of garbage, which is a problem other language designs do not suffer from nearly as much, letting them get away with simpler GC while maintaining high performance.
And Fortran isn't the only game in town for high performance numerical libraries ;) There is large incentive to reinvent there, as legacy doesn't matter if the new thing has more performance - Eigen, cuBLAS/CUDA, and others are in C++, not Fortran as far as I'm aware. Fortran has momentum no doubt, but it has been on its way out for a long time.
I would not put kotlin anywhere close to “popular” languages, and go is only getting into that category now. We will see whether they will continue to have support/interest 25 years down the line.
Also, due to the JVM’s flourishing multi-language support all these changes will greatly benefit all the other JVM languages as well. So in that view, I do think that the JVM has quite a future ahead of it because Valhalla will further improve the already killer performance.
> Unfortunately the more compatible you are with "V1" the more compromises you have to make. See: Kotlin, F#, TypeScript bending over backwards to maintain compatibility with the dominant language of their respective platforms.
For Kotlin and TypeScript, compatibility with the dominant language is the main reason for their success. For Typescript especially, the reason that it's so popular is how easy it is to migrate from JS to TS: JS is valid TS. I don't think the same is true for Kotlin. That combined with less "game changing features" in general (Java is already statically typed for example) probably explain why TS is way more popular in the JS world compared to Kotlin in the Java world.
> You have to admire the hutzpah of Python 3 level of changes.
You mean breaking backwards compatibility and dividing the ecosystem for relatively small changes? While I admire the self-confidence that it takes, I'm happy other languages are more reasonable in their approach. And the Python people seem to be more reasonable these days. Async was added without needing Python 4, and they're talking about performance improvements and multicore too. The rolling release probably helps a lot here, they can deprecate stuff slowly but surely.
> That said I'm surprised more languages haven't attempted the approach of Rust with 'editions'.
Editions can only affect some part of the language. From the Editions Guide [1]:
> The requirement for crate interoperability implies some limits on the kinds of changes that we can make in an edition. In general, changes that occur in an edition tend to be "skin deep". All Rust code, regardless of edition, is ultimately compiled to the same internal representation within the compiler.
Of course, a way to easily make skin deep changes is better than no way of making changes at all. But often, when a language changes, idioms do too and thus code has to be changed. For example, OCaml 5.0 will have effects for direct asynchronous IO. This will be backwards compatible, current monadic asynchronous code will still work, but people might want to rewrite their code in direct-style. C# introduced nullable reference types in C# 8.0. It's backwards compatible too, but you might want to update your code here too.
Kotlin is not Java vnext, first of all because Jet Brains is "married" with Google's fellowship of Kotlin, secondly because they seem to be focused into creating their own Kotlin based platform, finally OpenJDK will never be rewritten in Kotlin.
Rust editions are no different from selecting language versions, on their current state they require compiling from source with the same compiler, a very tiny set of language changes that might be supported.
Rust is quite different to my knowledge. You can mix n match libraries each using a different language version. I don’t know that the art of language changes is tiny either as some of these can be mutually incompatible [1]:
> Each project can opt in to an edition other than the default 2015 edition. Editions can contain incompatible changes, such as including a new keyword that conflicts with identifiers in code. However, unless you opt in to those changes, your code will continue to compile even as you upgrade the Rust compiler version you use.
Now granted it’s entirely possible that a given compiler version accidentally broke comparability when building in a different mode - that’s a bug. It’s also possible that this model does have a limit as to how breaking of a change the editions can have (eg probably the ownership model can’t change drastically) which is maybe what you’re trying to say? Certainly if I recall correctly there have been breaking changes in editions.
Totally different ecosystem that’s built around the concept of building from source. Binary libraries and alternate compiler implementations haven’t been a priority. In fact, multiple compiler implementations generally don’t exist until the language is much older than Rust is now. It’ll happen.
>Rust editions are no different from selecting language versions
Aren't they? Can you choose to compile Java 17 with Java 7-8 syntax, but still make use of Java 17's other features which don't break backwards compatibility? As far as I have understood, that's possible with Rust (though I might be wrong, I've never really tried using an older edition).
Even though you're right from the perspective you mentioned I want to disagree with you from the PoV of a regular developer. Scala is not "Java vnext" and has a learning curve rivaling C++. Kotlin comes naturally to Java developers and feels like Scala-flavored syntactic sugar. But unfortunately (e.g. I don't want to learn another ORM library on the JVM) they seem to be growing their own ecosystem indeed.
My perspective applies to Scala as well, to any guest language on JVM or any platform.
One lesson for Borland days is that I rather use the languages that are on the box than dealing with extra complexity of third parties in the platform.
My point of view is not always what wins out, and there is good to learn new approaches in programming that can be latter applied to the daily tools, but that is how I see things.
As a very pragmatic approach it's likely the most reasonable one. But it doesn't make it easier emotionally. It's like forcing yourself to use golang knowing fully well that you'd suffer through early-day-Java-level inferiority all over again.
The Borland example is confusing. The way I remember it their Pascal and C++ support was equally official. BC++3 was comparable to TP6 though the latter was less resource hungry (and at <700KB fit a single FD). Then for many years Delphi was well ahead of C++ Builder. But in the end professionally C++ skills were much more valuable.
Ten years ago I hoped that ORCL would acquire Typesafe and make Scala supported officially the way MSFT did with F#. Instead they left the space open for Kotlin to appear. And then antagonized GOOG into making it a juggernaut. There must be some Innovator's Dilemma-style explanation here but as a developer I regret it.
As a side-note I believe Martin made a huge mistake by allowing Haskell fans to highjack the narrative. His original strategy reminds me Elon's a little. Make industry finance and popularize high-impact type system research by using its impressive end result. Investing in Scala ten years ago prepared me well for Kotlin and modern Java (not to mention a few years of data engineering jobs around Spark and ML).
When new Java features like this are added, it also includes new features and optimizations in the JVM itself. Other languages that compile to the JVM can take advantage of these new features as well, either with no effort, or incrementally.
Only partially, for example whatever Kotlin code you would write to take advantage of those features won't be portable to Android thanks Google, which sucks for library writers.
Android is tiny compared to the Java ecosystem. Also, Google embraces kotlin mostly because they can’t keep up with JavaTM’s development. OpenJDK is at version 17 with many important changes, terrific performance improvements, etc. Not long ago android didn’t have lambdas, but it is still around Java 8. Kotlin is just syntactic sugar of some newer java features on top of older java.
> Kotlin is just syntactic sugar of some newer java features on top of older java.
Looks like someone hasn't actually spent time discovering how amazing Kotlin is.
I have, but it always felt like not going the whole length with the features, when Scala did much earlier. So if I want a “better Java” I really don’t see why should I stop at half way there. Java is a decent language nowadays so I either just use that or something quite different.
I'm sorry but this looks like an ad-hoc black or white thinking.
Language design is made of tradeoffs e.g between features and complexity.
However contrary to popular opinion, I'd say that Kotlin actually has almost all Scala features and notably has important features that Scala does not (such as coroutines, reified generics, compile time safe tail recursive, etc)
The most unique Scala feature seems to be implicits. While I'm not convinced by their usefulness, the feature will be available in Kotlin in a few months:
https://github.com/Kotlin/KEEP/issues/259
I'd even argue it is more useful in Kotlin since the language is state of the art regarding it's ability toward making DSLs.
The major features that Scala 3 has that Kotlin does not that I can identify are:
1) Pattern matching ->
Kotlin is almost there, it has destructuring, a when control structure, and can express ADTs via sealed classes/interfaces.
The limitations are: name based destructuring is not implemented.
No destructuring in when blocks.
However since java will be getting extensive pattern matching support in jdk 19/20
I expect Kotlin to catch up there and I recognize pattern matching can be a very useful feature.
2) union types and intersection types.
While those are useful in a structurally typed language such as typescript, I don't see much usefulness for them in Kotlin. You can express union types via sealed classes somehow. The compiler plugin arrow meta brings union type support but I haven't tested it yet. As for intersection types it is on the roadmap.
The main usefulness of union types I can see are the union of exceptions in catch blocks, that BTW Java supports, surprisingly.
3) higher kinded types
I don't see much usefulness in higher kinder types
What else could their use be beyond abstracting over collections? Every Kotlin collection implement the interface Collection<T> which enable this use case.
Again, higher kinded types are supported by the arrow-meta compiler plugin.
To me the very few pain points of Kotlin have nothing to do with functional features but are:
1)
Inability to deflare a function parameter as mutable.
Inability to declare for loop element as mutable.
Which in fact are because of IMHO religious functional toxic beliefs about immutability everywhere.
2) no package private modifier (although java 9 modules are supported and they are working on the issue)
> I'd say that Kotlin actually has almost all Scala features and notably has important features that Scala does not (such as coroutines, reified generics, compile time safe tail recursive, etc)
I really don’t see the point of coroutines, especially when a) project loom is on the horizon, b) higher kinded types make it quite trivial to implement as a library (answering your 3rd point)
I’m not sure about reified generics, they are very seldom a problem and frankly, type erasure is most often just misunderstood. Also, with pattern matching overloading is used less often so it is even less of a win.
Regarding compile time safe recursion, I’m pretty sure that Scala was actually the first bigger language implementing that (with an annotation), so it definitely has that.
So don’t get me wrong, Kotlin is a great language with some nice features, I just don’t see it doing anything novel to be honest (and that’s not a bad thing! Non-research languages rarely do anything truly novel!). I just feel like Scala does all of those with less special cases (eg. extension functions can be done with implicits so you get one feature doing many things)
No way —- Scala is a much bigger language. Kotlin usage is basically null outside of android (and even there, native may very well be bigger), while Scala is used by several companies for backend, data science, etc. I do know for a fact that eg. Morgan Stanley for example has many backends running it.
actually not true. ever since spring and everyone started supporting kotlin officially, a significant number of startups use Spring Boot, etc through Kotlin.
No, Google no longer uses anything that could have an issue with Oracle and haven’t been using for a long time. The lawsuit was about an older state of Java and Android.
On (1), all objects in Java are references, being a pointer to a pointer effectively. This indirection has a cost but has been incredibly useful historically because it enables moving allocation around the address space because none of the references change. They're effectively just pointers to a lookup table and it's the lookup table that changes.
As for the example of packing an array of Points into a contiguous block of memory to avoid the dereferencing, I'll be curious to see what the proposal is here. Now, for example, if you have a collection of Points (x,y), you can put in a 3DPoint (x,y,z) that extends Point. You can't do that if you've flattened storage, not without some overhead at least.
For years it's been the dream in Java to have true value Object types. You can do this in C++, which also allows you to allocate onto the stack. My position is that the complexity cost of this is massive. Copy constructors, move constructors, assignment operators, implicit constructors, implicit casts, etc. I mean you can have class A with instance a1 on the stack and a2 on the heap and pass &a1 and &a2 to a A* parameter and the callee has absolutely no idea of the lifetime of those or where they're allocated.
But still, it would be nice to pass around an SHA1 hash as a 20 byte value instead of a reference to an object that contains a reference to an array that contains 20 byte elements.
As for (2), this was a deliberate design choice in Java 5 (1.5) when generics were added. The decision was made, for better or for ill, to retain backwards compatibility through type erasure such that a List<T> is a List. This decreased the pain of upgrading and legacy code but created this ugly corner of primitive types.
C# 2.0 came along later and decided to go the other way such an IList<T> is not an IList (IIRC the types; I'm not a C# guy).
There are pros and cons.
The fact that certain values are guaranteed to satisfy reference equality is kind of weird and (IMHO) confusing ie new Integer(127).equals(new Integer(127)) is true but new Integer(128).equals(new Integer(128)) isn't necessarily true. I guarantee you a lot of people don't know that.
> On (1), all objects in Java are references, being a pointer to a pointer effectively. This indirection has a cost but has been incredibly useful historically because it enables moving allocation around the address space because none of the references change. They're effectively just pointers to a lookup table and it's the lookup table that changes.
There's only one level of indirection. References are simply pointers. You can't see the raw bits of the pointer without Unsafe, but that's how the Sun JVM implements references. (The implementation calls them "ordinary object pointers".) GC rewrites these pointers when it moves objects; there is no separate lookup table of all objects.
You might have been mislead by the existence of Object.identityHashCode(). The identity hash code is not a memory address and not guaranteed unique. It's an arbitrary value stored in some bits of the object's mark word and copied around when the object moves. That's how it remains stable across GCs.
Or you might be thinking of "compressed oops". Those are still pointers, but encoded as (object_address - start_of_heap) >> 3 to save space.
I think there is a global lookup table for interned strings and symbols loaded from class files, but slots in that table are not themselves pointed to by references.
IMO C# got this right. Admittedly I prefer C#, but working in Java right now type erasure really confuses me, especially working with streams. I can/will learn but it seems like a bad model.
That's definitely interesting, but I feel like C#'s approach of just creating a new set of containers for generics aged way better. It feels silly that I'm paying a tax in 2021 on a decision made in 2004 to be backwards compatible to code written in like 1996
> The language actually provides quite a strong safety guarantee for generics, as long as we follow the rules:
If a program compiles with no unchecked or raw warnings, the synthetic casts inserted by the compiler will never fail.
Huh. That was written in 2020, four years after it was shown how to write a very small program that "compiles with no unchecked or raw warnings", and yet "the synthetic casts inserted by the compiler" will fail at run time [0]:
class Unsound {
static class Constrain<A, B extends A> {}
static class Bind<A> {
<B extends A>
A upcast(Constrain<A, B> constrain, B b) {
return b;
}
}
static <T, U> U coerce(T t) {
Constrain<U, ? super T> constrain = null;
Bind<U> bind = new Bind<U>();
return bind.upcast(constrain, t);
}
public static void main(String[] args) {
String zero = Unsound.<Integer, String>coerce(0);
}
}
The sample program doesn't compile: Unsound.java:16: error: method upcast in class Bind<A> cannot be applied to given types; return bind.upcast(constrain, t);
As the article says, it was an unfortunate side effect of trying to maintain compatibility.
I imagine C# was able to learn from what was going on with Java at the time and not have to suffer the same fate. Or maybe it was just less popular and could force the issue (I’ve never dealt with it so I don’t know the history well).
The main use case of generics is collections, and if my memory serves me right, in .NET they simply created a new collections library (System.Collections.Generics) leaving the original intact (System.Collections) allowing old programs to work as before. It lacked 100% compatibility because you couldn't freely interchange the classes (without writing adapters) but from what I gathered, it was a small price to pay compared to type erasure (in my opinion) which prevented more aggressive runtime optimizations/evolution. Today you usually find the old collections in ancient software which hasn't been updated for years.
As it is usually brought up, not erasing types also comes with some potential cons: namely making CLR languages dependent on C#’s chosen variance model.
In Java, List<Cat> may or may not be the subclass of List<Animal>, but it is up to the List implementor. This way Scala/Kotlin/another JVM language is free to define their own variance model independent of the host language. C# did limit their language ecosystem with it quite a bit. (Afaik Scala for CLR stopped in part due to this).
In C# I believe you can optionally mark generic classes/methods as covariant or contravariant. Is that not enough or does it not get exposed in the CLR or something?
How do two languages with two different variance models interoperate in JVM if they share the same type but expect different behavior? Is it safe to share a list created in one language and pass into another if their variance models differ? Having an explicit variance model makes cross-language interoperability safer and easier (which was one of the main selling points of the Common Language Runtime), doesn't it?
Simply from an empirical point of view, the CLR is pretty much a desert compared to the flourishing JVM one so while it can be attributed to many things, I am really sure that explicit variance is not that attractive for language developers.
Yeah, java can change the variance model used by generics, but my point is that it is a language-level feature, not something fundamental at a JVM level, which is imo the correct decision.
Also, unfortunately arrays are covariant so Cat[] is a subclass of Animal[] (both in Java and C# actually), where your mentioned example indeed introduces a “poisoned” value, waiting for a classcastexception.
> They're effectively just pointers to a lookup table and it's the lookup table that changes.
What makes you think this is the case? Pointers to objects are direct, and garbage collectors freely change pointer values whenever it decides to relocate objects. Some collectors might use temporary forwarding pointers, to support concurrent collection.
It's sad they introduced two kind of classes: "value" and these "kinds" together with both classes and records, java becomes much more complicated IMO.
I think these are different semantic axes. There is one axis of less and less safety guarantees (identity, value, primitive) while records are more of a “here is a short syntax if you promise to keep some conventions”. But as mentioned in probably the second part, `value record` will also work properly.
While what you're saying true I'm pretty sure it's going to create a lot of confusion - these things share a lot - all of them have all the fields final for example, and feel like "simple" "data" classes.
I can see their reasoning to add value classes in addition to primitive classes, but the difference between them is going to create the most confusion, since they are identical in terms of requirements to classes themselves.
It will definitely give some additional learning to people, but I feel like their usage is still quite idiomatic. If someone didn’t even hear about these, he/she can still code as if they are identity-based classes for the most time (chances are they are not the one writing vanilla concurrent code with synchronized or using some advanced optimizations with identityHashCode), and conversely spamming value and primitive may fail at compile time or at worse, runtime with some meaningful error.
I'm really curious how this will influence the implementation of value classes and opaque types in Scala. I do hope that this will lead to a more efficient encoding, although I wonder how this would be possible without breaking backward compatibility.
I don't think it will affect opaque types implementation at all, since they exist only in compile time.
As for value classes - Valhalla is a reason (Scala's) value classes weren't removed from Scala 3 (in favor of opaque types), since they seem to be a good candidate to be implemented using Valhalla's classes.
I am not too knowledgable about Scala, but I think they only promise source-compatibility, so they are free to swap out the resulting binary to a more efficient implementation if the semantics are correct. Hopefully they did anticipate JVM-native value classes.
I do not know Java very well, but from the article I got the impression that in Java you could use List<?> as a reference in non-generic code to a List of arbitrary type. I.e. you could assign:
List<?> variable = new ArrayList<Integer>();
That is not possible with std::vector<std::any>> in C++ without converting and allocating a new vector. I am not familiar enough with the C++ standard to say it definitively but similar rules apply for vectors of pointers see https://godbolt.org/z/no3r67P5z
You are missing the part that in Java generics (at least into Valhalla) are a type system feature.
What the JVM ends up seeing is something like Java 1.4 bytecode would look like, and there is no need for allocations, because it is only references and generics don't support primitive types.
While C++ templates are monomorphic so a few more tweeks are indeed needed all the way down to machine code.
The principle of cleaning the underlying type applies though.
List<?> can be queried, e.g. you can write `Object obj = list.get(0)`, but you can't put anything into it, e.g. `list.add(anything)` will not compile. I'm not sure you can find similar concept in C++.
As mentioned, it is “7 PHd’s knitted together” worth of change, especially in a don’t break existing code way. It is arguably the most ambitious language related project out there.
I have been waiting for these fixes to the language since I was first introduced to Rust which got the type system “correct” where Java had left a lot to be desired.
The big thing that Java will get beyond generics and such being cleaned up for all types, is true Optional types. That is, a stack based Optional that is itself not null, but tracks something that might be null in the interior.
It will take a long time to update the ecosystem to correct this, but when it comes it will make Java a much “safer” language to develop in, that is a class of runtime errors can be avoided and caught at compile time. This will be great, and I look forward to the day when it comes.