I think opaque type aliases is one of Scala 3's most interesting features. Many use cases for it has already been discussed. One oppertunity that I think would be quite useful, while at the same time being a very good fit with the rest of the language, is the ability to expose members of the underlying type as members on the opaque type.
For example: The current imlementation of IArray uses extension methods to export methods from Array:
extension (arr: IArray[Byte]) def apply(n: Int): Byte = arr.asInstanceOf[Array[Byte]].apply(n)
...
extension [T <: Object](arr: IArray[T]) def apply (n: Int): T = arr.asInstanceOf[Array[T]].apply(n)
extension [T](arr: IArray[T]) def apply (n: Int): T = arr.asInstanceOf[Array[T]].apply(n)
...
extension (arr: IArray[Byte]) def length: Int = arr.asInstanceOf[Array[Byte]].length
...
extension (arr: IArray[Object]) def length: Int = arr.asInstanceOf[Array[Object]].length
extension [T](arr: IArray[T]) def length: Int = arr.asInstanceOf[Array[T]].length
Given that re-exporting methods on the underlying type is probably a quite common use case, I think exposing a subset of the underlying type would be very useful.
People have proposed the ability to export methods before, but I've not seen a bigger discussion about it:
https://contributors.scala-lang.org/t/having-another-go-at-exports-pre-sip/2982/22?u=mbloms https://contributors.scala-lang.org/t/proposal-for-opaque-type-aliases/2947/54?u=mbloms
Syntax is always hard, for now I'm borrowing the new syntax @odersky introduced for givens/structural instances. Using that, a concrete way to extend opaque types to support this could look like this:
opaque type Nat with {
def +(x: Nat): Nat
} <: Int = Int
opaque type IArray[+T] with {
def apply(i: Nat): T
def clone(): IArray[T]
def length: Nat
} = Array[_ <: T]
Of course, the transparent members can't override the implementation of underlying members, but they should be able to override the type signatures in a compatible way.
The overriding type signature of a transparent member must be compatible with:
- The underlying type as seen from inside the scope of the definition
- The lower bound as seen from outside the scope of the definition
- The upper bound as seen from outside the scope of the definition
One way to guarantee this could be that given the definition
opaque type T with {def m(x: A): B} >: Lo <: Hi = Rep
- There must be a member definition of
m
inRep
where the erasure ofm
inT
matches the erasure ofm
inRep
- There must be a matching member definition of
m
inLo
, andm
inLo
must subsume the definition ofm
inT
- If there is a matching definition of
m
inHi
, thenm
inT
must subsume the definition ofm
inHi
Where the definition of matching and subsumes is taken from the 5.1.3 and 3.5.2 respectively of the Scala Language Specification.
The fact that transparent members correspond to real members could be used to preserve them in joins:
trait C
def children: List[C]
object opaques:
opaque type A {
def children: List[A]
} = C
opaque type B {
def children: List[B]
} = C
import opaques._
def x: A & B = ...
def xs: List[A & B] = x.children
def y: A | B = ...
def ys: List[A | B] = y.children
Normally, the join of A
and B
would be Any
since A
and B
is unrelated to each other.
With transparent members, the fact that children
in A
and B
is owned by the same base class
is known on the outside. In other words the ownership of a transparent method is transparent.
Using my own notation I express the join of A
and B
like this:
The join of A | B
is {this: opaque C => def children: List[A | B]}
At first, it might not be obvious why this would be useful, but I would say there are
Something something blah blah I want this:
opaque type IArray[+T] = Array[_ <: T] with {
export this.{apply,clone,length}
}
And also this:
object Defs {
opaque type Name = String
extension (n: Name) {
export n.length
}
def newName(s: String): Name = s
}
About the supporting
export
clauses withinextension
blocks foropaque
types, let me put the concern that @smarter commented here from gitter.im/lampepfl/dotty:My reasoning for asking whether we could use
export
clauses for opaque types was to see if would selectively delegate some of the methods of underlying type without having to write all the wirings explicitly. We could just sayexport n.{length, charAt}
to expose two methods and keep everything else encapsulated.As @smarter mentioned in his comment, of course if those methods use the underlying type (in return or parameter positions) we would be leaking the abstraction that opaque type created. First solution comes to mind is to transform the extension method definition so that occurrences of underlying types in the extension method signature are replaced with the opaque type, but I imagine that could cause problems with variance (or maybe not, just thinking aloud).