Limit the implicit conversion of opaque types

I have been using opaque types a lot in my library and they are great, but I think the “implicit conversion” that occurs within the opaque source is far too lenient and in fact it caused me to have many hidden errors that I only discovered when I moved the opaque definition to a different file.

Consider the following simple example that compiles in Scala 3:

opaque type Foo[T] = Int

val f1 : Foo[1] = 1
val f2 : Foo[2] = f1

opaque type Bar = Int
val b1 : Bar = f1

The opaque has an invariant type argument which means that clearly it does not make sense that Foo[T] =:= Foo[R] unless T =:= R in any context, including within the scope where the opaque is defined. While I can understand the opaque concept should allow conversion Int ==> Foo[T] and Foo[T] ==> Int, I don’t think we should allow the transitive conversion of Foo[T] ==> Foo[R]. If the opaque types conversion to/from their underlying types was actually based on the implicit conversion mechanism, then the transitive conversion would have never been allowed, and for a good reason. This is why I think we should modify the behavior of opaque types with type arguments and prevent the code above from compiling.

For the same reason, I think we should not allow the transitive conversion Foo[T] ==> Bar even if they are defined in the same source file.

1 Like

@odersky I would like to get your take on this. Is it possible to limit the opaque “transparency” in such a way? Are there counter-arguments to why is this actually a good thing?

1 Like

While I can understand the opaque concept should allow conversion Int ==> Foo[T] and Foo[T] ==> Int , I don’t think we should allow the transitive conversion of Foo[T] ==> Foo[R]

The problem is that there is no conversion; that’s not how opaque types are defined. Instead it is a contextual equality (i.e. equality implied by the context). And there is no way to make that not transitive.

Then what about limiting the contextual equality to the companion object or even removing it completely and replacing it with an explicit conversion syntax that does not rely on asInstanceOf?

To me this shows that you want opaque types to be something completely different. That won’t happen.

But I believe for your use cases it would be good to keep to the convention that there should ne only one opaque type definition per scope. That should fix most surprises.

This would be strictly less powerful than the current behavior which allows defining an opaque type as a class member and having the implementations of methods of that type see through that opaque type (e.g. you could imagine a class with an opaque type Handle = Int which maintains a counter internally while allowing external users to pass around abstract Handle values). Whereas with the current behavior it’s easy to limit the scope of an opaque type by wrapping it in an object declaration.

4 Likes