This doc page is specific to Scala 3, and may cover new concepts not available in Scala 2. Unless otherwise stated, all the code examples in this page assume you are using Scala 3.
Opaque type aliases provide type abstraction without any overhead. In Scala 2, a similar result could be achieved with value classes.
Abstraction Overhead
Let us assume we want to define a module that offers arithmetic on numbers, which are represented by their logarithm. This can be useful to improve precision when the numerical values involved tend to be very large, or close to zero.
Since it is important to distinguish “regular” double values from numbers stored as their logarithm, we introduce a class Logarithm
:
classLogarithm(protectedvalunderlying:Double):deftoDouble:Double=math.exp(underlying)def+(that:Logarithm):Logarithm=// here we use the apply method on the companionLogarithm(this.toDouble+that.toDouble)def*(that:Logarithm):Logarithm=newLogarithm(this.underlying+that.underlying)objectLogarithm:defapply(d:Double):Logarithm=newLogarithm(math.log(d))
The apply method on the companion object lets us create values of type Logarithm
which we can use as follows:
vall2=Logarithm(2.0)vall3=Logarithm(3.0)println((l2*l3).toDouble)// prints 6.0println((l2+l3).toDouble)// prints 4.999...
While the class Logarithm
offers a nice abstraction for Double
values that are stored in this particular logarithmic form, it imposes severe performance overhead: For every single mathematical operation, we need to extract the underlying value and then wrap it again in a new instance of Logarithm
.
Module Abstractions
Let us consider another approach to implement the same library. This time instead of defining Logarithm
as a class, we define it using a type alias. First, we define an abstract interface of our module:
traitLogarithms:typeLogarithm// operations on Logarithmdefadd(x:Logarithm,y:Logarithm):Logarithmdefmul(x:Logarithm,y:Logarithm):Logarithm// functions to convert between Double and Logarithmdefmake(d:Double):Logarithmdefextract(x:Logarithm):Double// extension methods to use `add` and `mul` as "methods" on Logarithmextension(x:Logarithm)deftoDouble:Double=extract(x)def+(y:Logarithm):Logarithm=add(x,y)def*(y:Logarithm):Logarithm=mul(x,y)
Now, let us implement this abstract interface by saying type Logarithm
is equal to Double
:
objectLogarithmsImplextendsLogarithms:typeLogarithm=Double// operations on Logarithmdefadd(x:Logarithm,y:Logarithm):Logarithm=make(x.toDouble+y.toDouble)defmul(x:Logarithm,y:Logarithm):Logarithm=x+y// functions to convert between Double and Logarithmdefmake(d:Double):Logarithm=math.log(d)defextract(x:Logarithm):Double=math.exp(x)
Within the implementation of LogarithmsImpl
, the equation Logarithm = Double
allows us to implement the various methods.
Leaky Abstractions
However, this abstraction is slightly leaky. We have to make sure to only ever program against the abstract interface Logarithms
and never directly use LogarithmsImpl
. Directly using LogarithmsImpl
would make the equality Logarithm = Double
visible for the user, who might accidentally use a Double
where a logarithmic double is expected. For example:
importLogarithmsImpl.*vall:Logarithm=make(1.0)vald:Double=l// type checks AND leaks the equality!
Having to separate the module into an abstract interface and implementation can be useful, but is also a lot of effort, just to hide the implementation detail of Logarithm
. Programming against the abstract module Logarithms
can be very tedious and often requires the use of advanced features like path-dependent types, as in the following example:
defsomeComputation(L:Logarithms)(init:L.Logarithm):L.Logarithm=...
Boxing Overhead
Type abstractions, such as type Logarithm
erase to their bound (which is Any
in our case). That is, although we do not need to manually wrap and unwrap the Double
value, there will be still some boxing overhead related to boxing the primitive type Double
.
Opaque Types
Instead of manually splitting our Logarithms
component into an abstract part and into a concrete implementation, we can simply use opaque types in Scala 3 to achieve a similar effect:
objectLogarithms://vvvvvv this is the important difference!opaquetypeLogarithm=DoubleobjectLogarithm:defapply(d:Double):Logarithm=math.log(d)extension(x:Logarithm)deftoDouble:Double=math.exp(x)def+(y:Logarithm):Logarithm=Logarithm(math.exp(x)+math.exp(y))def*(y:Logarithm):Logarithm=x+y
The fact that Logarithm
is the same as Double
is only known in the scope where Logarithm
is defined, which in the above example corresponds to the object Logarithms
. The type equality Logarithm = Double
can be used to implement the methods (like *
and toDouble
).
However, outside of the module the type Logarithm
is completely encapsulated, or “opaque.” To users of Logarithm
it is not possible to discover that Logarithm
is actually implemented as a Double
:
importLogarithms.*vallog2=Logarithm(2.0)vallog3=Logarithm(3.0)println((log2*log3).toDouble)// prints 6.0println((log2+log3).toDouble)// prints 4.999...vald:Double=log2// ERROR: Found Logarithm required Double
Even though we abstracted over Logarithm
, the abstraction comes for free: Since there is only one implementation, at runtime there will be no boxing overhead for primitive types like Double
.
Summary of Opaque Types
Opaque types offer a sound abstraction over implementation details, without imposing performance overhead. As illustrated above, opaque types are convenient to use, and integrate very well with the Extension Methods feature.