Scala 3 puzzle about opaque type

How to get the following code to compile, given that foo is to be overridden in H? I cannot get any type annotation for override def foo to compile, let alone any method body…

trait A:
  type B
  type E = C[B]
  def foo: E

trait F extends A:
  opaque type B = Int
  def foo: G = new G()

trait H extends F:
  // Can the following line be fixed?
  override def foo: G & I = new G() with I

trait C[D]

class G extends C[Int]

trait I

it compiles if you remove opaque modifier from type member B from trait F

maybe you could use two type members, one that is opaque and one that isn’t?

1 Like

The problem is that in H the compiler can no longer see that G <: E even though foo: G in F and foo: E in A. So to satisfy the compiler foo needs to have type E & G & I in H, but you would never be able to create a new value of that type in H without casting. It’s a pretty strange use of an opaque type anyway.

A solution could be to lift the opaque type out of F so H can still see through it, like this:

object module:
  opaque type Q = Int
  trait A:
    type B
    type E = C[B]
    def foo: E

  trait F extends A:
    type B = Q
    def foo: G = new G()

  trait H extends F:
    override def foo: G & I = new G() with I

trait C[D]

class G extends C[Int]

trait I
1 Like

Let me give some motivation for this code:

A could represent some mathematical structure. For example a vector space. F could be an implementation of a vector space that uses a simple type internally to represent vectors (Int in the code).

Now vectors from different vector spaces should not be mixed up, as that doesn’t make sense (even when the vector spaces are isomorphic). For example, two vectors from different vector spaces should not be added together. Hence the opaque type.

H could be implementing an extension of the mathematical structure; for example vector spaces with cross products. To that end, it needs to refine the return type of foo, which could, say, be a way to get a basis for the vector space.

I don’t think the math can be modeled satisfactorily without opaque on B. Creating a surrounding shared scope would work, although it makes the abstraction leaky within that scope, forces all the code to be in the same file, and makes importing harder, amongst others.

I think the best is still an ugly cast:

override def foo: G & E & I = (new G() with I).asInstanceOf[G & E & I]

I do feel there is something wrong with opaque semantics that is highlighted by this code, though. Perhaps the compiler should know that, in that context, G is a subtype of E? Or perhaps opaque scopes could sometimes be protected?

1 Like

Maybe there’s an argument to be made for subclasses or subtraits to inherit the “opaque scope” for opaque types they inherit. But there are probably also use cases that require that not to happen.

I’m not sure that it’s really necessary for the compiler to check that G & I <: E though. The direct parent of H is F so in theory it should be sufficient to check that G & I <: G. But maybe there’s some edge case I’m not seeing.

1 Like

It compiles with covariant C.

trait C[+D]

enables

override def foo: G & E & I = new G with E with I

for reasons that escape me. I was curious what if B were wider than the arg in G.

opaque type B = P
class G extends C[Q]
trait P
trait Q extends P

Edit: meant to say something about “bridge” pattern. Although this example looks like extension, the encapsulation says composition, and I expected to arrive at something that uses export to easily create a delegate. That is, super.mkGE knows how to build G & E, and subclass delegates to it and implements I members.

1 Like