3 ms·
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 n
by dwohnitmok 7y ago
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.
- pron 7y ago> 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.
- dwohnitmok 7y agoSure, 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?
- pron 7y ago> 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.
- dwohnitmok 7y agoYou 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.