Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prove that two types have no subtyping relation in Scala?

Tags:

types

scala

(Note: The motivation for this requires a long and difficult explanation; you can find the full discussion on this Accord issue. It might not even be the right solution to the problem, but I believe the question is interesting in of itself.)

I'm looking a way to implement a binary operator such that the behavior depends on the type of the right-hand operand: one behavior if it is the same as the left-side operand, different behavior otherwise. As an example:

implicit class Extend[T](lhs: T) {
  def testAgainst(rhs: T) = println("same type")
  def testAgainst[U](rhs: U) = println("different type")
}

The first overload is more specific than the second, so you would expect an invocation such as 5 testAgainst 10 to trigger the first overload, whereas 5 testAgainst "abcd" would invoke the second overload. While this makes sense in theory, this will not compile because the erased signature is the same for both overloads.

I've managed to work around this in a way that requires adding a type parameter to the first overload, but that is exactly what I'm trying to avoid. A different solution would be to modify the generic overload to require compiler evidence that there is no subtyping relation between the types (the opposite of =:=, which unfortunately is not provided by the Scala library).

While it is generally pretty easy to encode subtyping relationships in Scala, I've found no way to encode the lack thereof. Is there any way to require that, for the second overload to be a candidate at compile time, neither T <:< U or T >:> U are true?

like image 201
Tomer Gabel Avatar asked May 12 '15 12:05

Tomer Gabel


2 Answers

If you want to enforce that two types are different strictly at compile time, then this is the question for you. Using one of the answers that defines =!=, we can imagine multiple methods that look like this:

implicit class Extend[T](lhs: T) {
  def testAgainst(rhs: T) = println("same type")
  def testAgainst[U](rhs: U)(implicit ev: T =!= U) = println("different type")
}

We can also do the type test within one method using TypeTag quite easily.

import scala.reflect.runtime.universe._

implicit class Extend[T: TypeTag](lhs: T) {
  def testAgainst[U: TypeTag](rhs: U): Boolean = typeOf[T] =:= typeOf[U]
}

You could then of course modify it to branch the behavior.

scala> 1 testAgainst 2
res98: Boolean = true

scala> 1 testAgainst "a"
res99: Boolean = false

scala> List(1, 2, 3) testAgainst List(true, false)
res100: Boolean = false

scala> List(1, 2) testAgainst List.empty[Int]
res102: Boolean = true
like image 89
Michael Zajac Avatar answered Nov 14 '22 05:11

Michael Zajac


The solution is actually pretty straightforward. Your only real problem is that both your overloads have the same erasure, which is only a problem to the compiler because of the limitations of the underlying JVM. As far as typing is concerned, having those two overloads is perfectly fine.

So all you have to do is to change the signature of one overload in such a way that is functionally equivalent. This can be done using an implicit parameter that will always be found (typically, standard library's DummyImplicit) or by adding a dummy parameter with a default value. So either of those is fine (I usually use the first version):

def testAgainst[U](rhs: U)(implicit dummy: DummyImplicit) = println("different type")

or:

def testAgainst[U](rhs: U, dummy: Int = 0) = println("different type")
like image 26
Régis Jean-Gilles Avatar answered Nov 14 '22 05:11

Régis Jean-Gilles