Search code examples
scalasingletonimplicitsingleton-type

In scala 2.11+ how to use exactly the singleton type as type evidence?


I'm writing a math library for complex vector computing, part of which looks like this:

object Example1 {

  val two = 2
  val three = 3

  // SU means 'special unitary group'
  trait SU_n[D <: Int] {

    def plus(v1: ComplexVector[D], v2: ComplexVector[D])(
        implicit ev: D =:= two.type
    ): ComplexVector[D] = {
      //TODO: some unspeakable magic here
      ???
    }
  }

  class ComplexVector[D <: Int: ClassTag](xyzw: List[(Double, Double)]) {

    {
      assert(xyzw.size.isInstanceOf[D])
    }
  }

  type Quaternion = ComplexVector[two.type]

  val alsoTwo = 2

  object SU_2 extends SU_n[two.type] {}
  object SU_Also2 extends SU_n[alsoTwo.type] {}

  object SU_3 extends SU_n[three.type] {}

  val q = new Quaternion(List(1.0 -> 2.0, 3.0 -> 4.0))

  {
    val v2 = SU_2.plus(q, q)
    val also_v2 = SU_Also2.plus(q, q)

  }

  val vec =
    new ComplexVector[three.type](List(1.0 -> 2.0, 3.0 -> 4.0, 5.0 -> 6.0))

  // This will break
  //  {
  //    val v3 = SU_3.plus(vec, vec)
  //  }
}

the infix type =:= is used to ensure that function plus won't be usable on vectors other than quaternions, when I compile it, I got the following error message:

Error: type mismatch;
 found   : <mypackage>.Example1.two.type (with underlying type Int)
 required: AnyRef
  type Quaternion = ComplexVector[two.type]

The strange thing is that I can't find anywhere in the implementation of infix class =:= that mandates its operand to be AnyVal, so why I'm getting this error? And how to fix/bypass it to achieve the requirement? (namely, create a function that can only be applied on quaternion)


Solution

  • Singleton types produced by .type don't work in quite the way you're imagining - in particular, they're not literal types. To see this, verify:

    val two = 2
    val alsoTwo = 2
    type V = two.type
    type W = alsoTwo.type
    implicitly[V =:= W] // will fail with an implicit error
    val test : V = 2 // will fail with "expression does not conform to type"
    def id(x: V) : W = x //will fail with "expression does not conform to type"
    

    Historically, this .type syntax was only meant to be used for AnyRef, with the compiler not exposing singleton types for primitive types. As of 2.13, this has changed in that the language now supports literal types, but it looks like the behaviour of .type is still the same.

    If 2.13 is an option, you can just write

      trait SU_n[D <: Int] {
    
        def plus(v1: ComplexVector[D], v2: ComplexVector[D])(
          implicit ev: D =:= 2
        ): ComplexVector[D] = {
          //TODO: some unspeakable magic here
          ???
        }
      }
    
      type Quaternion = ComplexVector[2]
    
      object SU_2 extends SU_n[2] {}
      object SU_Also2 extends SU_n[2] {}
    
      object SU_3 extends SU_n[3] {}
    

    and everything will work as intended. If you need to stick with 2.11 or 2.12, I suggest looking at shapeless if this is the route you want to go down.

    That said, as per my comment, this seems like a very odd way of solving the problem, since the idea of having a trait T[A] { def foo(a: A) } where calling foo will only compile if A is a specific type strikes me as fairly pathological. If you really want a plus method only accessible to the quaternions, you could do something like

    implicit class QuaternionSupport(quaternions: SU_n[2]) {
      def plus(quaternion1: Quaternion, quaternion2: Quaternion) : Quaternion = ???
    }
    

    and the method won't be present for other dimensions.