Non-extendable types, are they fixable?

OK, I know this has nothing to do with the type system (which is orthogonal to OOP and class/trait extension), but this has caused quite a lot of boilerplate for me in the past, while adding such capability doesn’t appear to take a lot of effort:

(Example):

object NonExtendableTypes {

  trait A
  trait B

  type C = A with B
  trait D extends C // doesn't work
  trait D extends A with B // works

  type E = A { type Sub = Int }

  trait F extends E // doesn't work
}

Instead of throwing an error, the compiler can easily create a temporary trait with the desired signature. The same mitigation has already been used for diamond inheritance long time ago, in Scala 2. So why is it hard to implement, given that it is totally consistent with the underlying type system?

1 Like

More problematic:

type AB = A & B

which is meaningfully different in appearance from

class C extends A, B

I wonder if Scala 2’s experimental type macros, which never made it to the experimental stage, would have served this purpose? I remember seeing a bit of code along the lines of, “when we have type macros, we won’t know the actual parents yet anyway.”

An example point of confusion for me with aliases is that in Scala 2, they were deemed not to have companions, but in Scala 3, they do serve as “anchors”, so their companions are searched for implicits.

(Implicit search sounds like some sort of police action.)

1 Like

“so their companions are searched for implicits”

That sounds tempting :smiley: but no the compiler doesn’t work that way:

object TypeCompanionScope {

  trait A

  trait B

  type C = A & B
  object C {
    implicit def empty[T <: C]: Seq[T] = Seq[T]()
  }

  class D extends A, B

  summon[Seq[D]]
}

: No given instance of type Seq[com.tribbloids.spike.dotty.conjecture.TypeCompanionScope.D] was found for parameter x of method summon in object Predef

The following import might fix the problem:

import com.tribbloids.spike.dotty.conjecture.TypeCompanionScope.C.empty

It’s not that it’s hard to implement. It’s that it is impossible to specify. Intersection types are commutative, but trait extension is not.

In concrete terms,

type C = A & B
type D = B & A

C and D are equivalent. They must be interchangeable in all contexts.

But

class C extends A with B
class D extends B with A

Here the definitions (contents) of the class are not equivalent. In C, term members of B override members of A, but it is the other way around in D.

4 Likes

It has to be an opaque alias. (Sorry, it’s been a couple of years. I remember learning the “anchor” terminology at the reference docs.)

trait F[A]

object X:
  opaque type T = Int
  object T:
    given F[T] = new F[T] { override def toString = "You found me!" }

@main def test() = println(summon[F[X.T]])

There was a ticket for Scala 2 that even got a Typelevel endorsement, but that was after the days of The Great Forking.

This is an example, for me, of something that seems reasonable to me but may be patently unsound to someone who has considered it in sufficient depth or abstraction.

I’m happy if I can keep up with the syntax.

1 Like