Can we use upper and lower bounds of type parameters for equivalence test?

I have the following code:

  trait TT
  class TT1 extends TT
  class TT2 extends TT
  class TT3 extends TT1

  val tt1 = TT1()
  val tt2 = TT2()
  val tt3 = TT3()

  def callAny[T <: TT](s:T) = ???
  callAny(tt1)
  callAny(tt2)

  def call1[T <: TT1](s:T) = ???
  call1(tt1)
  call1(tt3)   // avoid this
  //call1(tt2) // Ok won't compile

  def callOnly1[S <: TT1, T >: TT1](s:S with T) = ???
  callOnly1(tt1) // Ok
  callOnly1(tt3) // Should fail

Note that I want the last example to fail.

Can I use a combination of <:and >: to enforce equivalence? If so, how can I do this in Scala3 (dotty)?

TIA.

The combination of <: with >: will not work, as T >: TT1 means some supertype of TT1, which still will be a supertype of any class extending TT1.

Instead, for type equivalence checks you can use implicits / givens. The standard library defines the given instance =:=[T,U] for every type, where T and U are the same. In Dotty, you’ll use it like this:

def callOnly1[T](t:T)(using T =:= TT1) = t

This will cause a “Cannot prove that TT3 =:= TT1.” compile time error on the line you want to fail see Scastie

1 Like

@crater2150 Thanks. I have seen examples of the equivalence test. However, do you have any example that shows the use of a combination of <: with >:? I am trying to see how one could use this kind of constraint.

EDIT: neglected to mention that this is for Dotty/Scala 3
TIA

Sorry for the late answer, my mail notifications seem to have malfunctioned.

I don’t think I’ve seen any use of both <: and >: on the same type parameter. This Stackoverflow post has pretty much the same question as you. The first answer shows how to add an upper and lower bound to a type parameter via an implicit <:<, but also notes, that this can be circumvented easily and accidentaly: A val t: TT1 = new TT3 is obviously legal, but now the compiler sees t as an instance of T1 and will accept it as fulfilling a T1 lower bound. I noticed that the solution I posted above has the same problem, it will compile if you tell the compiler, that tt3 is a TT1: callOnly1(tt3: TT1) compiles.

I don’t remember any changes related to upper and lower bounds in Dotty. As those checks are at compile time, I don’t think it would be possible to guarantee such a lower bound outside the same compilation unit anyways. The static return type of some compiled function is what the compiler sees, and the actual type is determined at runtime. So sadly the only options seem to be runtime reflection or adding an additional marker trait to the subclasses that the function should accept.

1 Like

The link you provide is interesting. Your explanation makes things clearer. I guess the issue here is OO inheritance - we can always type an instance to one of its parents. I think I will add run-time checks based on sealed trait/classes.

Thanks you.