What's in a type alias

I would assume yes.

1 Like

In this case it seems this part has regressed, at least in Dotty 0.22.0-RC1, Prefix#Abstract no longer has Prefix in its implicit scope, while in Scala 2 it does. I’ve opened an issue report with minimized example in dotty tracker: https://github.com/lampepfl/dotty/issues/8396

1 Like

@eyalroth, I have a few polite corrections on this post.
I’ve shared some of your confusion in the past, also because using type members correctly is hard, but Scala abstract type members are one of its most advanced and most powerful features.

While they’re underdocumented and not easy to use, abstract type members are most certainly not a quirk. But their actual use case is not what you show; the point is to enable hiding their definition from clients. They also have a long history in module systems, going back to the 1974’s invention of abstract data types.

In fact, that example can be made to work, but it takes some doing. First, here is the code.

/ TypeMembers, TypeMembersDouble are unchanged.
trait TypeMembers {
  type TM
}

object TypeMembersDouble extends TypeMembers {
  type TM = Double
}

// The rest must be changed.
abstract class TypeMembersUser {
  val tm: TypeMembers
  def foo(x: tm.TM): Int = 1
  def bar(y: TypeMembersDouble.TM): Int = 2
}

object TypeMembersApp extends App {
  // Failing version:
  // val tmu =
  //   new TypeMembersUser { val tm = TypeMembersDouble }
  // Here, tmu has type TypeMembersUser, so tmu.tm.TM is ***abstract!***
  //
  // To make that type member concrete, we must *expose* more information in
  // types.
  //
  // Here is how to do it explicitly:
  //
  // val tmu : TypeMembersUser { val tm : TypeMembersDouble.type }  =
  //   new TypeMembersUser { val tm : TypeMembersDouble.type = TypeMembersDouble }
  //
  // I modified TypeMembersUser to make that type annotation expressible; I
  // tried TypeMembersUser { val tm : TypeMembersDouble.type } with the original
  // version, but didn't get it to work.
  //
  // That is verbose; however, making `tm` final will make the compiler infer for it a singleton type:
  //
  val tmu = new TypeMembersUser { final val tm = TypeMembersDouble }
  tmu.foo(2.5) // now compiles as well!
  tmu.bar(2.5) // compiles
}

Part of the problem is that in general the “content” of tmu.tm.TM (or in general path.TypeMember) only depends on the type of tmu, which surprises many people.
But that is a reasoned decision: the implementation of a value is not part of its public API, only its type is.

The other annoyance is that type inference, by default, is not very helpful in these cases.
That has been debated a few times; however, in many/most cases the default is arguably reasonable, because type inference should not expose too much information by default. It does make learning about type members very frustrating — but to some extent, docs could do more.

EDIT: as a reference on Scala abstract type members, I suspect I recommend https://www.cs.cmu.edu/~aldrich/courses/819/odersky-scala-theory.pdf as a starting point. While some of the technical ideas are outdated, it does spend more time introducing its ideas, since it was the first paper on the foundations of Scala. Next there’s The Essence of Dependent Object Types, but I think that is already more challenging and technical. But YMMV.

4 Likes

If I understand this correctly, then yes, I see now that type members provide capabilities that type parameters do not.

This particular capability can be very useful, especially when you want to add a type parameter to an already existing interface (trait) that is being, so that certain (new) methods will be typed according to that type:

// before re-factor
trait Response {
  def isSuccess: Boolean
}

// after re-factor
trait Response {
  type RType
  def isSuccess: Boolean
  def get: Option[RType]
}

With type members, there is no need to change all the Response symbols in the code that uses the trait.