Hi Scala Community!
This thread is the SIP Committee’s request for comments on a proposal to introduce Opaque Type Aliases in the language, a form of type aliases where the alias is only actually known in its companion object, while the relationship is opaque from the outside. You can find all the details here.
Summary
In Scala, we can define a type alias as follows:
object Enclosing {
type ID = Long
}
With that definition, Enclosing.ID
is treated everywhere as a simple alias to Long
.
While such a definition provides a convenient textual abstraction, the abstraction is not checked by the compiler, so we can erroneously use a normal number instead of an ID
, for example.
Opaque type aliases are similar, but the relationship between ID
and Long
is hidden from the program, except in the companion object of ID
, so that we can write:
object Enclosing {
opaque type ID = Long
object ID {
private var nextID: Long = 0L
def apply(): ID = {
nextID += 1L
nextID // compiles because within object ID we know that ID and Long are equivalent
}
}
}
import Enclosing._
val someID: ID = ID()
val anotherID: ID = 5L // does not compile
val someLong: Long = someID // does not compile
There is already a mechanism in Scala for similar abstractions: value classes declared with extends AnyVal
. The main issue with value classes is that they unpredictably box and unbox their underlying type.
Opaque type aliases, being, in the end, bona fide type aliases, box and unbox exactly when the underlying type would itself box or unbox (which means never for reference types, notably).
Another advantage of guaranteed equivalence at run-time is that it provides a good foundation for abstractions in interoperability scenarios. For example, in Scala.js, one might want to define a facade type for the set of JavaScript integers, even those that are bigger than Int
s. We cannot do that with a value class, because it would systematically box when given to JavaScript, therefore defeating the whole purpose. However, we can do it with an opaque type alias:
object JSTypes {
opaque type LargeInt = Double
}
An opaque type alias is almost always accompanied by a companion object, which can use extension methods to enhance the opaque type alias with relevant methods:
object JSTypes {
opaque type LargeInt = Double
object LargeInt {
def fromInt(x: Int): LargeInt = x.toDouble
def (x: LargeInt) toDouble: Double = x
def (x: LargeInt) + (y: LargeInt): LargeInt = x.toDouble + x.toDouble
...
}
}
Limitations
Due to their nature, opaque type aliases cannot redefine methods of Any
such as toString()
, equals()
and hashCode()
. Moreover, since they are (on purpose), indistinguishable from their underlying type at run-time, it is not possible to perform type tests on opaque type aliases, notably in pattern matching.
See the full proposal for all the gory details.
Implications
Together with extension methods, opaque type aliases replace virtually all use cases for value classes. We therefore expect that value classes will be progressively phased out from the language and deprecated. That is however out of the scope of this specific SIP.
Opening this Proposal for discussion by the community to get a wider perspective and use cases.