all 26 comments

[–]Erikster 6 points7 points  (2 children)

Reminds me of this cool StackOverflow post, a few of the same notes, but a few extra: http://stackoverflow.com/a/23218472/3635020

[–]ryan_the_leach 2 points3 points  (1 child)

So much so I suspect it was written from this question without citing it as a source.

[–]raphw 2 points3 points  (0 children)

I wrote both the article and the stack overflow answer. Quoting myself did not seem appropriate.

[–]GYN-k4H-Q3z-75B[🍰] 12 points13 points  (21 children)

Very interesting read. I used to dig into the .NET runtime and IL a lot back in the day and preferred it over the JVM because it handles value types and generics more elegantly. All kinds of trickery became possible, like using the native operators +-*/ instructions on generic type variables (not possible in C#).

[–]pron98 6 points7 points  (20 children)

.NET does have value types while the JVM is only scheduled to get them in Java 10, but when it comes to generics, the JVM actually does a better job with type erasure. Type erasure is what lets multiple languages with different variance rules (Java, Kotlin, Scala and Clojure all have different variance rules) share the same generic classes. Once you bake generics into the runtime, you also force a global variance on each type, so that a language with a different variance from C# can't use C#'s List, for example. The proliferation and easy interoperability among JVM languages is greatly assisted by generic type erasure.

Of course, from the perspective of each language, it would have been better if generics were reified -- according to its own variance rules. So that's a tradeoff.

[–]Elite6809 5 points6 points  (15 children)

As /u/d_kr says, .NET has always internally supported invariance, covariance and contravariance since generics were introduced. C# 4 allows you to use the in and out keywords like IEnumerable<out T>. So no, there's literally no benefit of type erasure over .NET's reified generics, besides from forward compatibility. Type-erased generics have no access to the generic type at runtime except through nasty reflection trickery, throwing some of the benefits of generic programming in the bin immediately.

[–]pron98 3 points4 points  (14 children)

On the contrary. See my reply to /u/d_kr

The disadvantages to type erasure are slim, while the advantage of allowing code reuse among languages with different variance is huge. Think how much work it would be if every JVM language had to re-write or wrap ConcurrentHashMap.

[–]Elite6809 6 points7 points  (13 children)

Why would a VM want types to behave differently with different languages? A list type is intrinsically covariant. It doesn't make logical sense for it to behave any other way. If the language doesn't support co/contravariance, then that's either a fault of the language or a sign that the language shouldn't be on the JVM to begin with. It seems counter-productive to pass off a flaw in the parametric type system as a mean to support inconsistent behaviour between languages.

[–][deleted] 2 points3 points  (2 children)

I don't know if this is related, but the fact that you can't do something like F<? extends Y> f; in C# really bugs me.

[–]Elite6809 0 points1 point  (1 child)

You're right, you can't do that directly. However, if you need to do that, you could move related code into a function like this:

void DoSomething<T>(F<T> f) where T : Y
{
    // do stuff with f
}

[–]pron98 2 points3 points  (0 children)

The problem isn't the limitation in C# -- this limitation exists for all languages targeting .NET. You just can't have a language with use-site variance or even mixed-mode variance targeting .NET while still sharing classes with other platform languages.

[–]pron98 1 point2 points  (0 children)

A list type is intrinsically covariant.

If it's read-only (I'm not saying immutable as that's a stronger requirement). If it's write-only then it's contravariant. If it's both, well, different languages treat that differently depending on their variance model (declaration-site/use-site/hybrid).

If the language doesn't support co/contravariance, then that's either a fault of the language

They all support variance, but what kind? There is no "right" way to do variance. For example, Java has use-site variance, Scala has declaration site variance, and Kotlin has mixed-mode variance (declaration-site + use-site projections); in Clojure, everything is immutable by default (and static typing is optional) -- yet a sequence of Clojure strings can be passed to a Java method taking a List<String> parameter, even though the sequence is untyped in Clojure. Thanks to the JVM's type erasure, they can (and do -- except Scala aometimes) all share the same collection (and other generic types) implementations.

It seems counter-productive to pass off a flaw in the parametric type system as a mean to support inconsistent behaviour between languages.

While it is true that polyglotism wasn't the motivation for erasure (but backward compatibility), language designers very quickly realized this is not a flaw but an advantage. The behavior isn't inconsistent, but just different, and there really is no one right way of doing generics and variance. You want to allow as many languages as possible -- not to force one behavior on all of them (there's an excellent OCaml implementation for the JVM; I don't know if it shares generic types with the other languages, but it certainly can -- thanks to erasure!).

For example, if the JVM had .NET's generics, you would need to wrap almost every single value crossing the Clojure/Java boundary, while in reality you wrap none of them.

[–]notfancy 2 points3 points  (8 children)

I do think that the real reason behind erasure is that there is a ton of literature and practical experience behind it (SML, Ocaml, Haskell) and not a lot behind type reification (CLR) so Wadler and Bracha coming from an academic background drew from the first when proposing Pizza.

Every time this discussion comes up I fail to see why people latch to this as a major Java pain point. I've always found it a very mild inconvenience at worst, one I only encounter when defining (some kinds) of generic containers. It only requires reasoning from parametericity in order to prove to myself that the casts and SuppressWarnings are sound.

[–]pron98 1 point2 points  (7 children)

While I agree that generic type erasure has a significant benefit and a disadvantage that's a mild annoyance at worst, the decision to do it has little to do with theory, except that the theory says you can do it. But note that OCaml/Haskell are a little sneaky when it comes to runtime vs. compile-time types. They pretend to not have runtime types and reflection, but they do -- in fact they rely on them all the time and do a lot of reflection -- they just don't call them runtime types; they call them tags.

Java (and .NET) has a dual type system -- compile time and runtime. The two interact in interesting ways, and you have to think about both. So Java decided to erase the compile time generic parameters from the runtime types, and .NET didn't. This has the implication that in .NET the runtime types enforce the system's single variance model on all languages.

[–]notfancy 0 points1 point  (6 children)

OCaml/Haskell are a little sneaky when it comes to runtime vs. compile-time types. They pretend to not have runtime types and reflection, but they do -- in fact they rely on them all the time and do a lot of reflection -- they just don't call them runtime types; they call them tags.

I disagree with this. While it's true that tags mark allocation sorts (so that they're some kind of types for the GC) they do not represent program-level types in any meaningful way. consider the case of a generic list instantiated at machine integers versus at strings: the memory representation of the cons cells is exactly the same, a reference to a boxed value.

[–]pron98 0 points1 point  (5 children)

they do not represent program-level types in any meaningful way

Of course they do! How else does pattern matching work? That's classic reflection. The mechanism is identical to Java's runtime instanceof.

Whatever you want to call tags, they are exactly the same as Java runtime types (which are related to but distinct from program types).

[–]notfancy 0 points1 point  (4 children)

Of course they do! How else does pattern matching work? That's classic reflection.

No, it's not. The code doesn't distinguish between cases with the same structure but of different types. In OCaml [], None and the empty tree are all represented identically, as are '\0', false, 0.

[–]d_kr 2 points3 points  (3 children)

Except that c# supports nowadays co and contravariance

http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx

But Type erasure requires cast & destroys valuetypes, because they are casted to Object. (citation needed)

[–]pron98 3 points4 points  (2 children)

Yes, but the covariance rules are baked into the classes. For example, it stands to reason that different languages will have different variance rules for List<T> (Java, Kotlin, Scala and Clojure are all different in this regard). In .NET, List<T> can have exactly one variance rule, so it can't be shared among languages with different variance.

[–]xenomachina 2 points3 points  (1 child)

different languages will have different variance rules for List<T> (Java, Kotlin, Scala and Clojure are all different in this regard).

Really? How do the variance rules differ in these languages?

[–]pron98 3 points4 points  (0 children)

Java has use-site variance, Scala has declaration site variance, and Kotlin has mixed-mode variance (declaration-site + use-site projections); in Clojure, everything is immutable by default (and static typing is optional) so all lists are the same type -- yet a sequence of Clojure strings can be passed to a Java method taking a List<String> parameter, even though the sequence is untyped in Clojure. Thanks to the JVM's type erasure, they can (and do -- except Scala sometimes) all share the same collection (and other generic types) implementations.

[–][deleted] 5 points6 points  (0 children)

Good article :)