News for hackers. Stuff that matters.
Re:Records Come to Java by bitL on Thursday January 16, 2020 @06:22PM (#22070180)
LOL, record was an intentional omission during designing Java, similar to output parameters in function calls. It's not like James didn't know about them or couldn't implement them.

The only truly problematic thing in the original Java design was type erasure, leading to mess in all JVM-based languages like Scala, Kotlin or Clojure, but it saved some memory, so it was a design compromise. C#/CLR chose the other option, making their VM more flexible.

Re:Records Come to Java by paulddraper on Thursday January 16, 2020 @07:23PM (#22070593)
> leading to mess in all JVM-based languages like Scala, Kotlin or Clojure

!!!

Type-erasure of generics is what gives the other languages flexibility.

Scala eventually gave up C# precisely because CLR reification proved a huge challenge with Scala semantics.

You may not like JVM type-erasure but Scala would literally not be possible without it.

---

If anything, I would argue that Java should have done full type-erasure, not just type-erasure of generic types. (That does come with downsides though.)

Re:Records Come to Java by bitL on Thursday January 16, 2020 @10:00PM (#22071466)
> Scala eventually gave up C# precisely because CLR reification proved a huge challenge with Scala semantics.

In other words, Scala was designed on a hack involving JVM's type erasure, then authors found out the hack can't be ported to CLR. That's another way to view it. Type erasure is a half-baked mess in original Java, it was causing headaches right with the first version of generics...

Re:Records Come to Java by pron on Thursday January 16, 2020 @10:07PM (#22071499)
Type erasure is not a hack. It's the most common technique in typed languages. It causes what is, at worst, a minor inconvenience in the Java language in exchange for making the Java platform a very attractive and very convenient target for languages. Reification -- and I believe .NET is the only platform with variance that does it -- makes a very inconvenient compilation target for any language not designed around it.
Re:Records Come to Java by bitL on Friday January 17, 2020 @12:39PM (#22076998)
That sounds as ex post facto justification. IIRC the designers hit the issue once they started implementing generics and the gravity of the situation became apparent just at that time. Then another set of hackers figured out a way to utilize it for language interoperability. I am not saying CLR's final implementation is great either, but it addresses some points JVM missed as they had time to learn from SUN's mistakes, while making their own errors in the process as well. Language/platform design is notoriously difficult and unpredictable.
Re:Records Come to Java by paulddraper on Friday January 17, 2020 @03:29PM (#22078868)
> That sounds as ex post facto justification.

The initial reason was indeed backwards compatibility. As it turns out, compatibility with Java 1 unintentionally yielded compatibility with future languages too.

Because of this flexibility (ex post facto notwithstanding), the JVM became the most popular VM target ever.

Note that GraalVM -- which from day 1 intended to be a multilingual VM -- also uses type erasure.

---

Know that there is more than conceptualization of a type system, and in particular a parameterized type system. C vs C++ vs Java vs Haskell. The more of this is baked into the runtime, the more reflection can be done at runtime, but the less interop can be done with other type systems.

(There is a reason why C not C++ is the de facto FFI standard and its not just age.)

Re:Records Come to Java by pron on Friday January 17, 2020 @01:04PM (#22077322)
The original issue was compatibility and interop between two languages with different variance strategies. It's just that those two languages were two versions of the Java language. Calling it a "justification" is also misplaced: baking a variance strategy into the runtime makes the runtime very over-fitted to a particular language; AFAIK this is only done by .NET. So if it's a "justification" it is a very reasonable one. I don't think either of the approaches is a black-and-white mistake, but I personally very much prefer Java's. The loss is a very minor inconvenience (mostly the inability to overload methods with different generic arguments, e.g. foo(List<String> x) and foo(List<Integer> x)), and the gain is that the Java platform is a great compilation target for many different languages. Whether or not those who made this decision had other languages in mind is irrelevant; it's a very good decision, even though some would prefer the opposite one. It's certainly not a mistake and not a hack. It's not a hack because it is a well-known, well-understood, common compilation technique; it's not a mistake because there is no alternative that's clearly better, and many (myself included) think it's significantly better than the alternative chosen by .NET.
Re:Records Come to Java by pron on Thursday January 16, 2020 @06:32PM (#22070255)
Type erasure of generic parameters is what allows the Java language, Kotlin, Clojure, JavaScript and other Java platform languages to interoperate so well and share data types with no runtime conversions despite having different variance strategies. By baking variance into the VM, .NET is much less flexible, as can be seen by how cumbersome it is to share data structures among different languages.
Re:Records Come to Java by dwohnitmok on Thursday January 16, 2020 @07:35PM (#22070661)
How is that? I don't have as much experience with .NET languages so I'm not sure of what difficulties lie in .NET interop, but from my very cursory experience with F# And C# I didn't find anything particularly difficult about using one's data structures in the other (granted they don't have different variance strategies).

What about Java's type erasure lets languages play better with variance strategies and data structures? It's not apparent to me that e.g. IronPython has a hard time sharing data structures with C# (although I haven't actually used IronPython FWIW). Why doesn't the usual strategy of upcast everything to `Collection<Object>` within a language where collections are covariant and then downcast as needed in a language that doesn't care about variance (e.g. a dynamic language) work?

Re:Records Come to Java by pron on Thursday January 16, 2020 @07:47PM (#22070732)
> granted they don't have different variance strategies

... because it's baked into the platform.

> It's not apparent to me that e.g. IronPython has a hard time sharing data structures with C#

https://ironpython.net/documentation/dotnet/dotnet.html#acce...

> Why doesn't the usual strategy of upcast everything to `Collection<Object>` within a language where collections are covariant and then downcast as needed in a language that doesn't care about variance (e.g. a dynamic language) work?

Because that does not work for mutable collections, or even for immutable collections created by, say, an untyped language. Clojure can create a list of strings and pass it to a Java method expecting a List<String>.

Re:Records Come to Java by dwohnitmok on Thursday January 16, 2020 @08:05PM (#22070855)
Right but my impression was that that was basically a performance optimization for IronPython, similar to type hints for Clojure and that they aren't actually necessary. IronPython was mainly what I was thinking when it comes to alternate variance strategies. I mean most typed languages I know of with stricter typing regimes than Java either all have the same variance schemes or eschew subtyping altogether, JVM, CLR, or otherwise.

Why doesn't that work for mutable collections (are you referring to the need to be invariant here?) or immutable collections? More specifically, why can't you always have `Collection<A>`, upcast to `Collection<Object>`, downcast to `Collection<B>` as an escape hatch? You break type safety but erased generics already break type safety (this is especially clear with C#'s choice to follow Java's convention on array variance and the pain that this causes when it's inconsistent with everything else in C#).

For passing back to a typed language, you add a downcast of `Collection<Object>` to `Collection<String>` and have no runtime conversion cost. Why doesn't that work?

EDIT: Ah I see, is the basic problem you don't get free (more accurately cheap) casts, especially with generics? I'm not familiar with how casting works in the CLR since the vast majority of my time is on the JVM.

Re:Records Come to Java by pron on Thursday January 16, 2020 @08:20PM (#22070947)
> are you referring to the need to be invariant here

Yes, or contravariant.

> erased generics already break type safety

They don't, they just don't enforce type safety across languages. BTW, most languages erase types (including Haskell).

> Why doesn't that work?

Because platforms with safe casts do a runtime instanceof test, and if the platform reifies generics, then it is not true that the runtime type of Collection<Object> can be cast to Collection<String>. But if the generic parameter is erased, they both have the same runtime type.

Re:Records Come to Java by dwohnitmok on Thursday January 16, 2020 @08:41PM (#22071081)
Sure, Haskell relies on parametric polymorphism and typeclasses to get away with full type erasure with opt-in reification via Typeable. C++ does something similar with templates and has opt-in reification with RTTI. The JVM is just weird because it has some type reification, but not all the way.

RE type safety, it doesn't really make sense to talk about type safety across languages since type safety is tied to a particular language's semantics. I would argue that every ClassCastException is a violation of type safety in Java the language even if it's reasonable at the JVM level but whatever that's just definitions diving.

I see. Basically what you're saying is that you don't really see examples of reified types with unsafe runtime casts? Presumably because that kind of negates the whole point of reifying the types in the first place?

Re:Records Come to Java by pron on Thursday January 16, 2020 @08:55PM (#22071150)
> The JVM is just weird because it has some type reification, but not all the way.

I don't know if it's "weird" given that there are only two examples, and they're different from one another. It's not like there's a norm here.

> I would argue that every ClassCastException is a violation of type safety in Java the language even if it's reasonable at the JVM level but whatever that's just definitions diving.

But you can't get a CCE in Java without explicit casts (or a compiler bug), just as in C#.

> Basically what you're saying is that you don't really see examples of reified types with unsafe runtime casts?

I'm not sure what you're asking.

Re:Records Come to Java by dwohnitmok on Friday January 17, 2020 @01:45AM (#22072433)
You can get CCEs without explicit casts or compiler bugs though. javac will catch the most glaring examples of generic type erasure with a warning.

  List<String> myStuff0 = new LinkedList<>();
  List eraseMyStuff = myStuff0;
  // Warning here
  eraseMyStuff.add(1);
  // CCE here
  String myString = myStuff0.get(0);
However, if you obfuscate it a little javac doesn't know about it anymore.

  List<String> myStrings = new LinkedList<>();
  List<Integer> myIntegers = new LinkedList<>();
  myIntegers.add(1);
  Stream.of(myStrings, myIntegers)
    // Wildcard capture
    .map(x -> x)
    .reduce((x1, x2) -> {
      // Would expect this line to blow up either at compile time or at runtime with reified generics
      list1.addAll(x2);
      return x1;
  });
  // Blows up with a CCE
  String result = myStrings.get(0);
The above is trick that was featured in some HN post from a while ago (maybe a year or so ago? Need to dig it up...) and is not an original idea of my own. It's essentially combining wildcard capture conversion with type erasure to delay a CCE away from the point of the problem. As I remember I think this isn't a compiler bug in the sense that this follows the Java spec.

I thought, however, there was an easier trick to get a CCE using type erasure of generics without warnings and without having to involve capture conversion, but it looks like I may have just been wrong about that as my efforts all ended up getting warnings from javac.

Of course whether any of this actually matters is a separate issue altogether. Java is still a workhorse language that powers a ton of software.

As for weirdness, I can't think of any other language with generics that preserves runtime type information without a way of preserving that for generics as well. Most as you point out either do full erasure (most stuff in the ML-inspired family) or either fully preserve all the information (of course all the .NET languages)/let you opt-in to preserving generics (Scala to some extent with TypeTags) or just forgo generics (Go).

For example in OCaml if you try to do an unsafe and incorrect cast you segfault. IIRC in GHC you end up with a crazy incorrect value (e.g. unsafeCoerce [1, 2, 3] :: Int results in some messed up integer).

My question was me thinking out loud; I was guessing that CIL bytecode has a instruction analogous to checkcast in JVM bytecode that you can just omit and then you can freely cast at will, potentially wreaking havoc if you cast incorrectly. I don't know if that's true. Basically I was separating the verification of type information from the storage of it at runtime. If the CIL separates them then the downcast trick works. If it doesn't the trick doesn't work.

Also in a way this unsafe cast is really just a run around reified types to begin with. I guess the only difference is that you still have storage of the types so you can do runtime reflection. On reflection, I can certainly see how this complicates implementing things like higher-kinded types (because you sort of end up implementing half of type erasure yourself) but I'm not sure it complicates marshalling ordinary data.

Re:Records Come to Java by pron on Friday January 17, 2020 @06:22AM (#22073745)
> You can get CCEs without explicit casts or compiler bugs though

Using raw types is cheating. It's essentially using a different language and, as you say, the compiler will tell you that you are.

> However, if you obfuscate it a little javac doesn't know about it anymore.

It's a bug (rather, a few bugs): http://wouter.coekaerts.be/2018/java-type-system-broken (I assume that's where you got the example from)

You shouldn't get a CCE without explicit casts, and if you do, that's a (soundness) bug in the Java compiler and/or spec. Those bugs aren't always fixed quickly when they're not very important in practice (if it's determined that it's hard to hit them by mistake, which is why it's taken 15 years to find them).

> Would expect this line to blow up either at compile-time or at runtime with reified generics

Reified generics have no impact at compile time. Everything a language with reified generics knows at compile-time, a language with erased generics also knows. However, a runtime with reified generics and a similar compiler bug would, indeed, throw at this line at runtime.

> As for weirdness, I can't think of any other language with generics that preserves runtime type information without a way of preserving that for generics as well.

There are good reasons for doing it the Java way: so that you don't bake a language's particular variance strategy into the VM. There are also good reasons for doing it the C#: it's slightly more convenient for that one language. I can't think of any other language with variance and a runtime type system that reifies generics for reference types. When all you have are two instances, it's hard to say which of them is weird.

> If the CIL separates them then the downcast trick works. If it doesn't the trick doesn't work.

I don't know CIL, but looking at how languages behave on .NET to interop, I wouldn't think that can work. It would totally break the safety of the runtime type system if you could cast without a typecheck.

> I guess the only difference is that you still have storage of the types so you can do runtime reflection.

BTW, you can reflect on generic type arguments in Java, as well (https://docs.oracle.com/en/java/javase/13/docs/api/java.base...). When you reflect on a method that returns a List<String>, it will tell you it returns List<String>, but you can't get such a Type from an instance of that list; you can only get a Class.