Search code examples
scalatypeclasstype-systemstype-level-computation

How to correctly use ambiguous implicits for type negation in Scala


Ultimately what I want to do is provide one implementation of a type class for some specific type T and another implementation for all other types which are not T. I thought (perhaps incorrectly) that the easiest way to do this would be to try type negation via ambiguous implicits as described in this question. However, if I accidentally omit the implicit type class declaration, my code will still compile (should it?) but include bugs as only one of the implementations is used.

This is how the context bound is defined:

scala> trait NotAnInt[A]
defined trait NotAnInt

scala> implicit def everythingIsNotAnInt[A]: NotAnInt[A] = new NotAnInt[A] {}
everythingIsNotAnInt: [A]=> NotAnInt[A]

scala> implicit def intsAreInts1: NotAnInt[Int] = ???
intsAreInts1: NotAnInt[Int]

scala> implicit def intsAreInts2: NotAnInt[Int] = ???
intsAreInts2: NotAnInt[Int]

scala> implicit def nothingsAreInts1: NotAnInt[Nothing] = ???
nothingsAreInts1: NotAnInt[Nothing]

scala> implicit def nothingsAreInts2: NotAnInt[Nothing] = ???
nothingsAreInts2: NotAnInt[Nothing]

At this point NotAnInt[T] is summonable for all T except Int/Nothing:

scala> implicitly[NotAnInt[String]]
res3: NotAnInt[String] = $anon$1@1a24fe09

scala> implicitly[NotAnInt[Int]]
<console>:16: error: ambiguous implicit values:
 both method intsAreInts1 of type => NotAnInt[Int]
 and method intsAreInts2 of type => NotAnInt[Int]
 match expected type NotAnInt[Int]
       implicitly[NotAnInt[Int]]
                 ^

scala> implicitly[NotAnInt[Nothing]]
<console>:18: error: ambiguous implicit values:
 both method nothingsAreInts1 of type => NotAnInt[Nothing]
 and method nothingsAreInts2 of type => NotAnInt[Nothing]
 match expected type NotAnInt[Nothing]
       implicitly[NotAnInt[Nothing]]
                 ^

Now I have my NotAnInt context bound defined I can create my type class with its implementations:

scala> trait IntChecker[A] { def isInt(): Boolean }
defined trait IntChecker

scala> implicit val intIntChecker: IntChecker[Int] = new IntChecker[Int] { override def isInt = true }
intIntChecker: IntChecker[Int] = $anon$1@585dd35c

scala> implicit def otherIntChecker[A: NotAnInt]: IntChecker[A] = new IntChecker[A] { override def isInt = false }
otherIntChecker: [A](implicit evidence$1: NotAnInt[A])IntChecker[A]

This type class can be used as expected:

scala> def printIntStatus[T: IntChecker](t: T): Unit = { println(implicitly[IntChecker[T]].isInt()) }
printIntStatus: [T](t: T)(implicit evidence$1: IntChecker[T])Unit

scala> printIntStatus(3)
true

scala> printIntStatus("three")
false

However, the following also compiles:

scala> def printIntStatusWithBug[T](t: T): Unit = { println(implicitly[IntChecker[T]].isInt()) }
printIntStatusWithBug: [T](t: T)Unit

scala> printIntStatusWithBug(3)
false

scala> printIntStatusWithBug("three")
false

I would not expect this second function to compile as there should be no implicit IntChecker[T] available. I expect everythingIsNotAnInt is the cause of this problem but I can't think of a way around this.

I'm interested in why this approach fails as well as alternative methods on how to achieve the same thing. Thank you.


Solution

  • Consider the following alternative implementation (which uses Sabin's type inequalities)

    trait =!=[A, B]
    implicit def neq[A, B] : A =!= B = null
    implicit def neqAmbig1[A] : A =!= A = null
    implicit def neqAmbig2[A] : A =!= A = null
    
    trait IntChecker[A] {
      def isInt(): Boolean
    }
    
    object IntChecker {
      import scala.reflect.ClassTag
      implicit val intIntChecker: IntChecker[Int] = () => true
      implicit def notIntIntChecker[T: ClassTag](implicit ev: T =!= Int): IntChecker[T] = () => false
    }
    
    def printIntStatus[T: IntChecker](t: T) = implicitly[IntChecker[T]].isInt()
    
    import IntChecker._
    printIntStatus(3)
    printIntStatus("three")
    

    which outputs

    res0: Boolean = true
    res1: Boolean = false
    

    however the buggy implementation where we forget IntChecker bound

    def printIntStatusWithBug[T](t: T) = implicitly[IntChecker[T]].isInt()
    

    should not compile due to having T: ClassTag bound in

    implicit def notIntIntChecker[T: ClassTag](implicit ev: T =!= Int)
    

    giving compiler error

    could not find implicit value for parameter e: IntChecker[T]
    def printIntStatusWithBug[T](t: T) = implicitly[IntChecker[T]].isInt()
                                                   ^