Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

C# was designed for the CLR and the CLR was designed for C#. F# on the other hand has 2 generics systems in the same language, one inherited from C# and one for ML-style Hindley–Milner based on type-erasure and they don't interact well.

Java's generics can't be built on top of the CLR's reified generics, but this goes for other languages as well - Haskell, ML, Scala, you name it. Basically, reified generics work well in case you design the language in combination with the VM, otherwise it becomes a huge PITA.

FYI, the CLR's reified generics is one reason for why the JVM is a better target for other languages. For dynamic languages, they are a problem because the bytecode generator has to work around them. For static languages, such as Scala or anything in the ML family, it's a problem because either (a) they are designed for co/contra-variance or (b) they don't support higher-kinded types or (c) they don't support rank-2 types or (d) in case you do need co/contra-variance rules, for some reason Microsoft chose to restrict the declarations only to generic interfaces and delegates, so you can't design a List<+T> class for example (to be honest, I'm not sure if this last one is a limitation of the language or the CLR).

Also, having reified generics is less important than you think. Scala for example can do specialization for primitive types and because the type-system is much stronger, coupled with an awesome collections library and pattern matching, the flow of the types is much better. For example, I can't remember any instance in which I felt the need to do a (obj isInstanceOf List<int>) check, immutable collections can be covariant without issues and for everything else there are type-classes, something which C# sorely lacks.

IMHO, in spite of people's opinion on the matter, Java's type-erased generics is one of its best features.



Also, having reified generics is less important than you think. Scala for example can do specialization for primitive types and because the type-system is much stronger, coupled with an awesome collections library and pattern matching, the flow of the types is much better. For example, I can't remember any instance in which I felt the need to do a (obj isInstanceOf List<int>) check, immutable collections can be covariant without issues and for everything else there are type-classes, something which C# sorely lacks.

I was under the impression that Scala just used Manifests and type tokens (a la my colleague Neal Gafter's Super Type Tokens) to get around the problem of erasure. I don't see any evidence that this actually works better, though.


Yes, it also has TypeTags, allowing you to inspect exactly what the compiler saw at compile-time in detail. That's not really what one uses in practice, e.g...

    scala> val list = List(null, 1, 2, "any")
    list: List[Any] = List(null, 1, 2, any)

    scala> list.collect { case x: Int => x }
    res0: List[Int] = List(1, 2)

    scala> Vector(null, "hello", 3).collectFirst { case i: Int => i * 2 }
    res1: Option[Int] = Some(6)

    scala> import collection.immutable.BitSet
    
    scala> BitSet(1,2,3).map(_ + 1)
    res2: immutable.BitSet = BitSet(2, 3, 4)

    scala> BitSet(1,2,3).map(_.toString)
    res3: immutable.SortedSet[String] = TreeSet(1, 2, 3)
To be honest, tags are indeed needed for creating arrays, since they are reified by the JVM:

    scala> import scala.reflect.ClassTag
    scala> def myAwesomeArray[T : ClassTag] = new Array[T](10)

    scala> myAwesomeArray[Int]
    res4: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

One common problem is that overriding of parameters doesn't work for containers due to type erasure, so you can't have overrides on type parameters, because the JVM gets in the way (i.e. the following does not work):

    def sum(list: List[String]) = ???
    def sum(list: List[Int]) = ???
However, Scala has something more powerful (type-classes):

    trait Monoid[T] { 
      def plus(x: T, y: T): T
      def zero: T 
    }

    implicit object IntMonoid extends Monoid[Int] { 
      def plus(x: Int, y: Int) = x + y
      val zero = 0 
    }

    implicit object StringMonoid extends Monoid[String] { 
      def plus(x: String, y: String) = x + y
      val zero = "" 
    }

    def sum[T : Monoid](list: List[T]) = {
      val monoid = implicitly[Monoid[T]]
      list.foldLeft(monoid.zero)(monoid.plus)
    }

    scala> sum(List("a", "b", "c", "d"))
    res5: String = abcd

    scala> sum(List(1,2,3,4))
    res6: Int = 10
But wait, we can go much further with implicits that actually dictate the return type:

    trait SetBuilder[T, R] {
      def empty: R
      def buildInstance(param: T): R
      def concat(s1: R, s2: R): R
    }

    implicit object IntSetBuilder extends SetBuilder[Int, BitSet] {
      def empty = BitSet.empty
      def buildInstance(param: Int) = BitSet(param)
      def concat(s1: BitSet, s2: BitSet) = 
        s1 ++ s2
    }

    implicit object StringSetBuilder extends SetBuilder[String, Set[String]] {
      def empty = Set.empty[String]
      def buildInstance(param: String) = Set(param)
      def concat(s1: Set[String], s2: Set[String]) =
        s1 ++ s2
    }

    def buildSetFrom[T,R](params: T*)(implicit builder: SetBuilder[T,R]): R = {
      if (params.nonEmpty)
        builder.concat(
          builder.buildInstance(params.head),
          buildSetFrom(params.tail : _*)
        )
      else
        builder.empty
    }  

    scala> buildSetFrom(1,2,3)
    res7: scala.collection.immutable.BitSet = BitSet(1, 2, 3)

    scala> buildSetFrom("a", "b", "c")
    res8: Set[String] = Set(a, b, c)
Scala in general allows for code that is much more generic than C#, without having the need for reified generics.


And F* has dependent typing. I was never claiming that reified generics was necessary for stronger typing, but I'm also not convinced that it's an impediment.

At worst I could see how it could be a trivial implementation detail, but the fact that Scala went out of their way to use tokens and manifests suggests that the functionality is desired. Whether that's provided by the runtime or by the compiler writer is only a difference in effort (or rather, whose effort).


This is the first time I've heard of F*. Interesting. Is it usable?


Not really. It basically generates a number of type (in)equalities and outsources finding the solution to Microsoft Z3 theorem prover, which you cannot use for anything commercial.


This seems inaccurate - generics aren't a problem for F# at all (the integration between the .NET and ML type systems is seamless here, as far as I'm aware). Indeed the people behind the .NET generics design (Don Syme and Andrew Kennedy in MSR) were coming from an ML background, and Don is the primary researcher behind F#. The big ML-.NET type system mismatches have to do with things like pervasive overloading in .NET making type inference difficult, nothing to do with reified generics.


Don Syme, Andrew Kennedy and many others that work for Microsoft are awesome. This doesn't make it any less of an appeal to authority that's not valid. The reality is that their creativity was limited by the constraints of .NET

> The big ML-.NET type system mismatches have to do with things like pervasive overloading in .NET making type inference difficult, nothing to do with reified generics

You've hinted at a side-effect, but no, it has to do with subtyping [1], which naturally leads to co/contra-variance [2]. Or to put it bluntly, in a nominal type system that has sub-typing, Hindley-Milner type-inference is not only "difficult" but actually impossible. And because F# had to be interoperable with .NET, then it needed C# generics. Pity, because there's many things that F# misses, like Ocaml functors, structural typing, type-classes, higher-kinded types and the list can continue. And I just love how List.sum<^T> is defined [3] (static member, FTW).

[1] https://en.wikipedia.org/wiki/Subtyping

[2] https://en.wikipedia.org/wiki/Covariance_and_contravariance_...

[3] http://msdn.microsoft.com/en-us/library/ee353634.aspx


Sure, subtyping adds additional pain points (mostly separate from overloading, BTW). But I think this is mostly orthogonal to reified vs. erased generics. As it is, the .NET runtime supports reified generics and declaration-site variance (albeit limited to interfaces and delegates), and to my knowledge there's nothing stopping a different language/runtime from implementing reified generics even with usage-site variance (and Scala's type system only has declaration-site variance, anyway, AFAIK).


Generics are "so seamless" in F# that they ship in fact with two versions of Generics.


What does this even mean?



I wouldn't call that "two systems"; statically resolved type parameters are an extension of .NET generic parameters, not a separate system. In particular, a single definition can use both statically resolved and non-statically resolved parameters (e.g. in `let inline f x y = x + x`), and statically resolved type parameters can be used freely where normal .NET generic type parameters are expected (e.g. in `let inline g x = x + id x`). And these distinctions are fairly transparent to users in most cases anyway, since type inference is usually capable of inferring the kind of parameter needed.

In any case, it is true that F#'s type systems has concepts that the CLR doesn't natively support, but I don't see how this demonstrates any weaknesses in the CLR or F#. The exact same thing is true of Scala on the JVM, as far as I can tell - how are erased generics an improvement?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: