What's in a type alias

Type members are never really ‘abstract’, type Permission is sugar for

type Permission >: Nothing <: Any

Which is a valid bound, just uninhabitable.
You may think that eventually the type must be aliased to something to be inhabitable, but this is not the case because of lower bounds:

trait Xa {
  type T >: Nothing <: Any
  def t(): T
}

trait Xb extends Xa {
  override type T >: Option[Int] <: Any
  def t(): T = Some(5)
}

This means that a bounds-only type member is not an “abstract” type member, and there’s no way to write down an “abstract” type member – all non-alias type members have bounds.

1 Like

It’s not uninhabitable, for any reasonable definition of “uninhabitable”. It’s just an incompletely-specified type. In theory, an object’s type member may have any bounds as long as the lower bound is a subtype of the upper bound.

You are misusing Scala terminology. An abstract type member is a type member which is not definitionally equal to another type (i.e., not a simple alias). All abstract type members have bounds, regardless of whether they are specified explicitly or not.

But I don’t think they are different. Type alias is just a name for a “concrete” type member. We don’t have separate keywords or concepts for abstract vals or defs either.

It’s worth noting that in DOT, the language formalizing the core of Scala’s type system, type aliases are really just a special case of type members, where the two bounds are equal.

That is, in DOT, { type A = T } is syntax sugar for { type A >: T <: T }.
The fact that these are not exactly equivalent in actual Scala is more of an implementation restriction (for compiler performance reasons). In theory, they are the same.

2 Likes

What I mean is, there is no non-bottom value that you can assign to this type member when it’s declared with default bounds and not refined further, without using .asInstanceOf. I do think I can call that “uninhabited”, but maybe I’m wrong.

Now, when the lower-bound is raised, there now appear values that can be assigned the type of the type member without casting.

You do not contradict me, what you say is that an “abstract type member” is not abstract in the same sense that a method signature is abstract i.e. not incomplete or uninstantiable, which is exactly what I was trying to convey. “abstract” for type member is not the same “abstract” as in “abstract class” or “abstract method”, that’s why an object can contain an “abstract type member”, but can’t contain an “abstract method signature”.
This is not the first time I see this confusion, many people think that a abstract type member declaration is an “interface” and overriding it with a type alias is an “implementation” – so every abstract type member has to be followed by an alias override. This is a wrong intuition, type members don’t have a “declaration”/“interface”/“abstract” state, they’re always complete types

I see what you mean, but it’s not quite right (though I’m being a bit pedantic). Here is a way to construct a value of that type:

trait T { type P >: Nothing <: Any }
val ev: { val t: T; val p: t.P } =
  new { object t extends T { type P = Any }; val p = 42 }

In ev, p has type t.P where t is of type T.

I’m not trying to contradict you, just to let you know that in common usage, “abstract type member” means a type that’s not concrete (i.e., for which we know only its bounds).

I did mean not refined further, i.e. no overrides. Given object t { type P >: Nothing <: Any }, there is no non-bottom value that can be ascribed as t.P without a cast.

True, they can be seen in this way, but the analogue to val / def is not complete.

A val declared inside an object can be used anywhere a class val is used (same for defs). This is not true for types:

trait TypeMembers {
  type TM
}

object TypeMembersDouble extends TypeMembers {
  type TM = Double
}

class TypeMembersUser(tm: TypeMembers) {
  def foo(x: tm.TM): Int = 1
  def bar(y: TypeMembersDouble.TM): Int = 2
}

object TypeMembersApp extends App {
  val tmu = new TypeMembersUser(TypeMembersDouble)
  tmu.foo(2.5) // doesn't compile
  tmu.bar(2.5) // compiles
}

Furthermore, type parameters have all the capabilities that abstract type members have and more, while type aliases cannot be achieved by any other means:

trait TypeParameters[TP]

object TypeParametersDouble extends TypeParameters[Double]

class TypeParametersUser[TP](tp: TypeParameters[TP]) {
  def foo(x: TP): Int = 1
  def bar(y: TypeParametersDouble.TP): Int = 2 // doesn't compile
}

object TypeParametersApp extends App {
  val tpu = new TypeParametersUser(TypeParametersDouble)
  tpu.foo(2.5) // complies!
}

So in reality, I believe that type aliases are much more commonly used, and serve a role that no other feature provides, while abstract type members are more of a quirk that has an easier and more powerful alternative.

Yes, I do agree with that. Note that “non-bottom value” is redundant as there is no bottom value in Scala (null is not of type Nothing). Also, we say a type is uninhabited, not uninhabitable.

Bound does not mean concrete:

object a {
  val i: Int // "bound" & not concrete (abstract) - doesn't compile
}

Like @LPTK said:

… in common usage, “abstract type member” means a type that’s not concrete (i.e., for which we know only its bounds).

This is an irregularity, especially as the entire motive behind abstract type members (AFAIU) was to align types with values and functions, in the sense that they all have the ability to be abstracted either via parameters or abstract members.

You are confusing “bound” from the verb to bind and the noun “bound”, which refers to the upper and lower bounds of an abstract type.
(Unless by “bound” you meant that the value has an ascribed type?)

Also, if you re-read my messages, you’ll see that type aliases are conceptually like type members, so I do not see the point you are making.

@sjrd understood, this is my last message on the topic.

Bound is indeed quite the ambiguous word, so I’ll refrain from using it for the sake of this discussion.

My point is that in both cases – abstract val and abstract type – we have a non-concrete definition that limits / restricts the (eventual) concrete value to a certain range. The irregularity is that an object can have these non-concrete (abstract) types but not vals (nor defs).

@sjrd Can you perhaps migrate this discussion to another thread? I’d like to keep discussing this, it’s interesting :slight_smile:

1 Like

I extracted a separate topic for this discussion. I hope I did not make any mistake in selecting the set of messages that belong here.

Also feel free to suggest a different title.

3 Likes

is, in DOT, { type A = T } is syntax sugar for { type A >: T <: T }

Maybe it should be, but it’s also odd for a type member to lose its path prefix and become completely transparent upon obtaining matching bounds.
For example, this code works in Scala 2, but not in Dotty: https://scastie.scala-lang.org/y4ITSKlNTdKYLIzHG0pXSQ
I would expect at least the call that explicitly ascribes the singleton type of the variable to succeed – because clearly DebugProperties IS a prefix of the singleton .type, so it should be in implicit scope – with or without the type Property member, but unfortunately this doesn’t work even with explicit ascription neither in Scala 2 nor 3.

The reason it does not compile in Dotty is because you have not imported the implicit DebugProperties.PropertyOps into the scope of the application. Dotty simplified the implicit resolution rules a little, so that things defined in the prefixes of types are no longer part of the implicit scopes of these types.

Both in Dotty and Scala 2 using A >: T <: T is expected not to lose the connection to the middle A while using A = T will aggressively dealias (for performance reasons) and is expected to lose the connection. Yes, I consider it unsatisfactory, but I’m not sure there is a good solution in sight.

1 Like

This seems to apply only to non-opaque type members. (And if this were applied to opaques they would become useless) Out of these summons only InnerType doesn’t summon:

trait Show[A] { def show(a: A): String = a.toString }

object Prefix {
  trait InnerTrait
  opaque type InnerOpaque = Unit
  type InnerType
  
  given Show[InnerTrait]
  given Show[InnerOpaque]
  given Show[InnerType]
}

object App extends App {
  println(s"implicit: ${summon[Show[Prefix.InnerTrait]]}") // ok
  println(s"implicit: ${summon[Show[Prefix.InnerOpaque]]}") // ok
  println(s"implicit: ${summon[Show[Prefix.InnerType]]}") // error
}

This is not documented anywhere and is inconsistent with opaque and class members. Are you sure this is intentional and not a regression?

Hmm, if in the previous scastie Property is declared opaque the example now works:

trait DebugProperties {
  opaque type Property >: String <: String = String
}

This has an onerous side-effect of actually adding a reason to declare an opaque type with fully transparent bounds…

This looks weird. It seems that the type-prefix-in-the-implicit-scope rule actually is still working for everything but transparent type aliases. Not sure that’s intended. Perhaps worth opening an issue or asking on gitter. I may also be missing something (I never manage to fit the whole set of implicit resolution rules into my head at once).

Haha, nice hack. I hope this is fixed at some point, though; I wouldn’t want a future where people declare their type aliases in this abscons form just to abuse implicit scopes.

From what I remember the only change to prefixes was that enclosing package objects aren’t members of implicit scope anymore, but perhaps I missed that change.

Yeah, it should just work without opaque

It is intended. The reason is that transparent type aliases are ephemeral - the compiler is free to dealias at any point. So we do not want to hang additional functionality (like companion scopes) on such type aliases, since any such functionality would be fragile.

The other thing that has changed in 3.0 is that package prefixes of types no longer contribute to the implicit scope, but normal prefixes are unaffected by this.

1 Like

What about abstract type members like

trait Prefix {
  type Abstract
  implicit def abstractGiven: TC[Abstract]
}

Shouldn’t they retain their implicit scope (abstractGiven should be available for Abstract as in Scala 2) when they are visible as abstract, i.e. when they’re not visibly aliased to anything?