I want to implement two type aliases that "refer" to the same underlying type (so they behave the same, have the same methods) but cannot be mixed together.
Here is the code example :
object example {
case class foo(value: Double):
def +(that: foo): foo = foo(value + that.value)
opaque type myType1 <: foo = foo
opaque type myType2 <: foo = foo
extension (d: Double)
def toMyType1: myType1 = new myType1(d)
def toMyType2: myType2 = new myType2(d)
}
import example.{myType1, myType2}
import example.{toMyType1, toMyType2}
val a1 = 1.5.toMyType1 //a1: myType1
val a2 = 2.4.toMyType1 //a2: myType1
val b1 = 3.2.toMyType2 //b1: myType2
val a3 = a1 + a2 //a3: foo but wanted a3: myType1
val c1 = a1 + b1 //c1: foo but wanted COMPILATION ERROR (can't add "myType1" and "myType2")
Wanted behaviour :
myType1
can be added together, resulting in a new myType1
(same for myType2
)myType1
and myType2
cannot be added together (compilation error)Idea behind this: you can't add meters and seconds together, even though adding meters with meters or seconds with seconds work the same.
For the record: I thought that using opaque
for the type aliases would make the compiler treat myType1
and myType2
as two different types (forbidding addition between them), even though they are effectively the same type.
You should use type class. Since you are using Scala 3 you can use improved support for type classes in Scala 3:
object example {
opaque type myType1 = Double
opaque type myType2 = Double
extension (d: Double)
def toMyType1: myType1 = d
def toMyType2: myType2 = d
// type class definition
trait Semigroup[A]:
extension (a1: A) def +(a2: A): A
// type class instance
private val semigroupDouble = new Semigroup[Double]:
extension (a1: Double) def +(a2: Double): Double = a1 + a2
given Semigroup[myType1] = semigroupDouble
given Semigroup[myType2] = semigroupDouble
}
import example.*
val a1 = 1.5.toMyType1 //a1: myType1
val a2 = 2.4.toMyType1 //a2: myType1
val b1 = 3.2.toMyType2 //b1: myType2
a1 + a2 // myType1
//a1 + b1 // compilation error