scalatypestype-alias

How to implement type aliases that behave the same but cannot be used together


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 :

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 myType1and myType2as two different types (forbidding addition between them), even though they are effectively the same type.


Solution

  • 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