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
minRepwhere the erasure ofminTmatches the erasure ofminRep - There must be a matching member definition of
minLo, andminLomust subsume the definition ofminT - If there is a matching definition of
minHi, thenminTmust subsume the definition ofminHi
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
}
I think the problem with export clauses in general. For instance consider this example which do not use opaque types:
https://gist.github.com/mbloms/31f8c8c32cfb0dec2d636d5f55ad9499
Before I realised
Builderhas themapResultmethod, my first thought was simply to make an anonymous builder and export methods fromArrayBuilder. If it wasn't formapResultI would have had to redefine every method inBuilderthat returnthisexplicitly.Perhaps in this special case, this could be solved by attaching signatures in the export clause, but that would not match existing syntax. In any case, there are definitely cases where you don't necessarily want to blindly replace the original type with the opaque version.