8 ms·
Nice, clear summary. I'd like to throw some more thoughts out there, though. Focusing overmuch on what Java did leaves the picture somewhat incomplete. First
by bunderbunder 8y ago
Nice, clear summary.
I'd like to throw some more thoughts out there, though. Focusing overmuch on what Java did leaves the picture somewhat incomplete.
First off, I've seen some people suggest that type erasure is a problem because it leads to a loss of type safety. This isn't really true. Haskell, for example, also preserves no run-time type information. The difference there is really about strength of typing - Haskell is very strongly typed, whereas C is weakly typed, and Java falls somewhere in the middle.
Second, the Java convention of passing Class variables into generic methods in order to give them all the type information they need:
<T> T getT(Class[t] clazz) { ... }
looks a bit like the C/C++ type erasure pattern that the author mentioned in passing at the start of the article, if you squint at it.
My own sense is that the reason why type erasure was a problematic choice in Java is that it places Java in this uncomfortable middle ground where you have neither the robust compile-time type checking of a strong statically typed language like Haskell, nor the robust run-time reflection of a modern dynamic language. The end result, being robust at nothing, did not turn out to be the best of both worlds.
- hota_mazi 8y ago> First off, I've seen some people suggest that type erasure is a problem because it leads to a loss of type safety. This isn't really true. I'd argue it's the opposite. Giving your programs the ability to access reified types at runtime weaken the robustness of your code, because you can second guess the compiler and introduce crashing code, such as incorrect type casts. Reification also comes at a cost: performance, more problematic interoperability (Scala could notoriously not be ported to .net because it was not possible to make its type system work properly on a reified platform). Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation.
- seanmcdirmid 8y agoScala could be ported .net easily enough, but it couldn’t interop with reified generic APIs. MS actually doesn’t use generics as aggressively as it could in its API for similar reasons (eg there are two APIs for collections that use genetics and don’t).
- bunderbunder 8y agoMy understanding is that the problem was, more specifically, that they couldn't easily get Scala's types mated to .NET, and that was in turn an implication of the decision to use IKVM to run the Java binaries for Scala's standard libraries on the CLR. Which I could see being a problem, because then you need to figure out how to smoothly marshal data back and forth between (type erased) Scala collections and .NET functions, which typically expect reified types these days. If they had gone with producing a .NET native version of Scala's standard libraries, they might have had easier success. But it also would have been a Pyrrhic victory, because you'd have lost the ability to run any existing Scala code, which would have scads of dependencies on other bits of the JDK as well, on Scala for .NET. At which point, what's the point? I expect that porting F# to the JVM would encounter similar problems. And I expect that which platform you want to blame them on depends on which one is your home platform. IMO the real story here is that the two big managed platforms are effectively walled gardens, by virtue of having VMs that implement such very high-level bytecode. Both of them effectively demand that all languages running on them can interact with, in effect, a thoroughly object-oriented ABI. There being approximately as may flavors of OOP as there are OO languages, that creates a situation where, if your language isn't set up to play nice with that platform's particular brand of OOP, you're going to have a bad time.
- dtech 8y ago> But it also would have been a Pyrrhic victory, because you'd have lost the ability to run any existing Scala code, which would have scads of dependencies on other bits of the JDK as well, on Scala for .NET. At which point, what's the point? Ask the parade of JVM languages porting themselves to native and JS runtimes.
- bunderbunder 8y agoThose are both very different use cases.
- tutanchamun 8y ago
- dllthomas 8y ago> Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation. Do you have evidence for the "because"? I agree that erasure is a sound approach for type system representation when designing a language, but I thought that backwards compatibility was explicitly a concern in the case of Java. Also, that good languages can be built with type erasure doesn't mean it's a good fit for the Java context where the runtime is doing a lot of dynamic dispatch based on tags that now only represent a part of the type.
- hota_mazi 8y ago> Do you have evidence for the "because"? I agree that erasure is a sound approach for type system representation when designing a language, but I thought that backwards compatibility was explicitly a concern in the case of Java. When the debate was raging around 2004 when generics were being discussed, Neal Gafter offered a proposal of a reified JVM that would maintain backward compatibility. Erasure was ultimately chosen over that proposal. Backward compatibility was not the issue: technical soundness was.
- dtech 8y ago> Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation. This is false. I cannot find the source at the moment, but in a talk about implementing improved lambda support in Java 8 a Java compiler dev clearly labeled type erasure as technical debt they have to work around. It frequently bites them when they try to implement new things, and causes weird interaction with new language features they have to work around.
- merb 8y agowell to be fair, with this debt they came up with something really useful. indy and lambdametafactory
- jayd16 8y agoIn other talks Java language devs mention it as a conscious design choice.
- Someone 8y agoReading JSR-14 (https://download.oracle.com/otndocs/jcp/7566-generics-0.1-prd-spec-oth-JSpec/ https://download.oracle.com/otndocs/jcp/7566-generics-0.1-pr...), it was a conscious choice: ”Supporting generic types at run time seems undesirable for the following reasons: - Lack of experience with such constructs in widely used languages - Burden of extensive VM changes on vendors throughout the industry - Increased footprint on small devices - Decreased performance for generic methods - Compatibility” However, that doesn’t rule out it is technical debt, too. “Conscious design choice” and “technical debt” do not rule out each other.
- slavapestov 8y ago> Reification also comes at a cost: performance On the contrary, reified generics give the implementation more leeway with representation choices which can help with performance. In Java for instance a `List<Integer>` is stored as an array of boxed Integer objects, without easily being able to store the values inline.
- bunderbunder 8y agoThat can be done even more easily with properly erased generics. This is another of those unfortunate implications of Java's having disappeared into this ugly chasm between static and dynamic typing. It doesn't fully erase types, so you can't do like C++ or Haskell do and just generate a completely different type, because it wouldn't be compatible with any functions that work with the generic type. At least, not without breaking backward compatibility, which was the whole point of doing type erased generics in the first place.
- dtech 8y agoI'm curious, what is not "proper" about Java's erased generics? Reified types also bring other advantages, e.g. runtime reflection can be useful and can enable meta-programming and allows check binary compatibility of dynamic linking. So gives that you want a language with reified types, losing that reification with generics creates awkward cases in the languages and prevents possibility for optimization.
- bunderbunder 8y agoThey're not erased all the way, they're only erased down into the root type for all objects. So you've still got some type information left (namely, that it's an Object), and any types that are not Objects are officially banned from the generics system. Hence the need to dress them up as washerwomen and sneak them in through the service entrance.
- slavapestov 8y ago> This is another of those unfortunate implications of Java's having disappeared into this ugly chasm between static and dynamic typing. It doesn't fully erase types, so you can't do like C++ or Haskell do and just generate a completely different type, because it wouldn't be compatible with any functions that work with the generic type. Full monomorphisation is not compatible with separate compilation though.
- paulddraper 8y ago> Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation. That statement is obviously wrong since Java did not choose type erasure. https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#getClass-- https://docs.oracle.com/javase/8/docs/api/java/lang/Object.h... In Java 5, generic types were added. And those Java chose to erase. So Java chose both: to reify and erase.
- int_19h 8y ago> Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation. I wouldn't call it a "sound approach for type system representation", when it can't even consistently represent the fact that generic interfaces instantiated with different type parameters are different interfaces. I mean, it's obviously true given that any parameter-dependent method signatures are different, yet you can't implement Foo<Bar> and Foo<Baz> on the same class. > Reification also comes at a cost: performance What's the performance cost of reification? When comparing C# to Java, the only thing I can think of is that C# has an advantage in performance, because, due to generics being reified, its JIT can compile instantiations separately.
- yawaramin 8y ago> you can't implement Foo<Bar> and Foo<Baz> on the same class. Works for me: import java.util.List; class Test { public void setListBoolean(List<Boolean> list) {} public void setListString(List<String> list) {} public List<Boolean> getListBoolean() { return null; } public List<String> getListString() { return null; } public static void main(String[] args) {} }
- saagarjha 8y agoYour functions are named differently, though.
- int_19h 8y agoYou're using it, not implementing it. Try: class Test implements List<Boolean>, List<String> { ... } For another example, try your snippet, but rename setListBoolean and setListString to just setList (so that they're overloads).
- yawaramin 8y agoOK I see what you mean now, but that is the combination of type erasure and allowing method overloading that is causing the issue, not the erasure itself. For example, look at a language that doesn't allow overloads, like OCaml. Its types are fully erased, allowing very efficient compiled or transpiled code, e.g. https://reasonml.github.io/en/try?rrjsx=false&reason=LYewJgrgNgpgBAFRgZwC5wLxwN4Cg5yzrIyoAyAlmgBRRWoBch91aAThQHYDmAlL5jgApZADooIbgCZq+AnABEAZVLM0CgDRyCdNHN4BuXAF8jQA https://reasonml.github.io/en/try?rrjsx=false&reason=LYewJgr...
- naasking 8y ago> Contrary to popular belief, Java's choice of erasure was not dictated by backward compatibility concerns but because erasure is a sound approach for type system representation... ...for languages that don't have casting and have correct variance. Java fails on both.
- skybrian 8y agoHaskell implementations typically pass around dictionaries at runtime when you use type classes. Since there is one per instance, this could be thought of as a form of type reification. https://www.schoolofhaskell.com/user/jfischoff/instances-and-dictionaries https://www.schoolofhaskell.com/user/jfischoff/instances-and...
- 0db532a0 8y agoI don't agree that this is type reification. The dictionaries are resolved at compile time. They may indeed be passed to a function at run-time, but they are not exposed to the user. They are simply a compile-time substitution of abstract function invocations with the implementations according to the chosen type. For actual reification of types in Haskell, you can look at Haskell's Data.Typeable library.
- skybrian 8y agoWell, it's an internal data structure needed at runtime that corresponds to a type, which seems close enough for a rough analogy. But I agree that it's not a language feature.
- int_19h 8y agoIt depends on whether you're looking at it from the implementation perspective, or from language user perspective. To the implementation, a dictionary carrying type information (or anything derived from that, such as e.g. method dispatch tables) at runtime is type reification.
- 0db532a0 8y agoThe concept of reification exists only in the context of a chosen language and its universe of objects, and it doesn't extend to the language's implementation. Given the language Haskell and its universe of objects, Data.Typeable provides a reification of Haskell's types into physical objects inside that universe. The type class dictionaries are not physical objects inside this universe.
- 8y ago
- pron 8y agoRegardless of questions of what should or shouldn't be done, there is a serious issue with reifying generic types with type parameters that support subtyping, as the JVM does. It requires deciding on a variance policy (to determine the subtyping relationships among the generic types), and different languages choose different variance policies to fit with their overall design. Reifying generic types in this case would bake a single variance policy into the runtime, which would hinder the ability of different languages to share data structures. If the type parameters do not support subtyping, this is not a problem, and, indeed, when generic types will be instantiated with parameters of Java's upcoming value types (that cannot be subtyped), they will likely be reified and specialized.
- int_19h 8y ago> It requires deciding on a variance policy (to determine the subtyping relationships among the generic types) Why does it? In C#, for example, variance policy for generic type parameters is expressed explicitly - invariant by default, covariant or contravariant if you ask for it. While C# also has reified generics, I don't see why this couldn't be done with non-reified ones.
- sreque 8y agoC#'s reified generics are actually a problem for other languages that want to run on the CLR, and in fact those languages generally have data structures with no reified generics (the data structures just store type Object and do perform dynamic casting). In order to work with the CLR bytecode generated outside of their compilers, these languages unfortunately also have to provide providence for understanding generic data structures. The JVM has been more successful than the CLR at hosting other languages, and having unreified generics I believe has contributed to that success. For instance, I have seen both the creator of Scala and one of the JRuby contributors both say they prefer JVM generics to CLR generics.
- int_19h 8y agoI still don't see what that has to do with variance policy specifically. You're basically saying that reified generics are bad, because they require all languages on a given runtime to be aware of them to interoperate. Which is a valid point, but if you are aware of generics, then CLR lets you express whatever variance you want - including none. And so long as your language has CLR interface casts at all, you automatically get support for that variance at runtime, even if the type system of the language doesn't reflect such relations statically; so e.g. you might not be able to implicitly convert IEnumerable<Derived> to IEnumerable<Base>, but you should always be able to explicitly cast IEnumerable<Derived> -> Object -> IEnumerable<Base>.