Inferred type issue

Hello,

In the code below, I would have expected the line marked “Compiles ok” to NOT compile. Since the parameter 99 is an Int, shouldn’t the type parameter T be inferred as an Int, and fail the test “Int <: C” ? I really would have expected the two lines not to compile.

Anyone would know what signature to give to the method contains_strict to enforce the type check whether the [T] type parameter is provided or not?

Thank you!

PS: If I run the line “Seq(C()).contains_strict(99)” with an additional implicit ClassTag argument for type T, the runtime class is really “int”, so the compiler must assume that T is Int. Why the test T <: A does not fail is thus a mystery.

extension [A](seq: Seq[A]) def contains_strict[T <: A](x: T): Boolean = seq.contains(x)

object Test:

  case class C()

  def main(a: Array[String]): Unit =
    // Compiles ok.
    Seq(C()).contains_strict(99)
    // Compile error: Type argument Int does not conform to upper bound Test.C
    Seq(C()).contains_strict[Int](88)

I guess what is happening is that the whole expression infers A to be Any.

What happens if you split it into multiple expressions, like this?

val seq = Seq(C())
seq.contains_strict(99)

Of course not, we are well-typed - C | Int it is! :wink: (Can be checked with the -Xprint:typer compiler option.)

I don’t see a way around other than explicitly forcing T (as in the OP) or forcing A (but thus waiving extension method convenience):

contains_strict[C](Seq(C()))(99)
1 Like

I got an answer from Perplexity AI, indicating that Scala was using a hint from the Seq.contains method, which uses an argument being a supertype of A, rather than being a subtype of A, where A is the type of the sequence elements.

So I wrote another example that does not refer to a Seq, and the same issue arises. So it is not related to an argument being a supertype; AI was wrong.

In the expression “b.myext(a)”, the type X is supposed to be inferred as B, the type Y is supposed to be inferred as A, and the constraint Y <: X (A <: B) should fail because A is not a subtype of B.

Anyone knows how to enforce the constraint check even when the type Y is not explicitly mentioned?

Thank you.

object Typing:

  extension [X](x: X) def myext[Y <: X](y: Y): Unit = println(s"$y <: $x")

  class A:
    override def toString(): String = "A"
  class B extends A:
    override def toString(): String = "B"

  def main(args: Array[String]): Unit =
    val a: A = A()
    val b: B = B()
    a.myext(b)
    a.myext[B](b)
    // b.myext[A](a) <- Does not compile
    b.myext(a) // Should not compile (I think!)

Output:

B <: A
B <: A
A <: B

I found a way to enforce typing using implicit classtag parameters.

I would be curious to know why the introduction of xtag and ytag below enforces typing checks.

Thank you.

JL

object Typing:

  extension [X](x: X)(using
      xtag: ClassTag[X]
  )
    def myext[Y <: X](y: Y)(using
        ytag: ClassTag[Y]
    ): Unit = println(s"$y <: $x")

  class A:
    override def toString(): String = "A"
  class B extends A:
    override def toString(): String = "B"

  def main(args: Array[String]): Unit =
    val a: A = A()
    val b: B = B()
    a.myext(b)
    a.myext[B](b)
    // b.myext(a) // <- Does not compile
    // b.myext[A](a) <- Does not compile

Previous thread in this area: Type-safe contains - Language Design - Scala Contributors

1 Like

The minimal code is:

import scala.reflect.ClassTag

extension [X](seq: Seq[X])(using
    xTag: ClassTag[X]
) def contains_strict[Y <: X](y: Y): Boolean = seq.contains(y)

object Test:
  Seq(3.1415926).contains_strict(1) // OK
  // Seq(1).contains_strict(3.1415926) DOES NOT COMPILE
1 Like
  1. Seq(1) creates a Seq[Int]

  2. The compiler infers X = Int from the receiver

  3. You’re passing a Double as the argument, so Y = Double

  4. The constraint requires Double <: Int - ouch!!

  5. Even if the compiler tried to widen to AnyVal, the inference algorithm binds X from the receiver type first, and then checks if the argument satisfies Y <: X

The asymmetry comes from Scala’s type inference order for extension methods. The type parameter X is primarily inferred from the receiver type Seq[_], and while there’s some flexibility due to covariance, the specific value that gets passed as an argument constrains whether a valid solution exists. In the second case, there’s no way to satisfy both the sequence is Seq[Int] and Double <: X simultaneously, since Double is not a subtype of Int.

1 Like

Nice - and somewhat weird… It looks like the ClassTag forces resolution and pinning of X at the extension level already, whereas otherwise this is postponed and reconciled with the Y binding later on.

Curiously, when changing the explicit using parameter to a context bound (which I naively would’ve thought of as equivalent), the trick breaks, because the compiler pushes the ClassTag down to the method signature. Compare:

extension [X](seq: Seq[X])(using xTag: ClassTag[X])
extension [X >: Nothing <: Any](seq: Seq[X])(using xTag: ClassTag[X])
  def contains_strict[Y >: Nothing <: X](y: Y): Boolean =
    seq.contains[X](y)
// ...
contains_strict[String](
  Seq.apply[String](["" : String]*))(
  ClassTag.apply[String](classOf[String]))[String](42)

vs.

extension [X: ClassTag](seq: Seq[X])
extension [X >: Nothing <: Any](seq: Seq[X])
  def contains_strict[Y >: Nothing <: X](y: Y)(using ev$1: ClassTag[X]): Boolean =
    seq.contains[X](y)
// ...
contains_strict[String | Int](
  Seq.apply[String](["" : String]*))[Int](42)(
  ClassTag.apply[String | Int](classOf[Object])
)

:thinking:

1 Like

This issue seems to be 100% unrelated to extension methods.

My guess is that it is related to how the scala3 typer works. When trying to resolve an expression, it will find the “most common denominator” of the types involved in order to accept the expression, in the absence of explicit types, when types have to be inferred. It seems that types are not inferred the same way when their specifications come from two different “sites”. We can see it at work below. In the case of “C”, the constraints on X and Y come from one “site”, in the case of “D”, they come from two “sites”. In the case of “C”, we can see that the term b is considered as a A, while in the case of “D”, the term b is really considered a B.

I do not really understand why the presence of the classtags is determinant to make this code works as expected (cases D and F below ).

Sorry for the approximate language, my knowledge of compilers is limited to Wirth’s PL/0 :wink:

package cc.lemieux.poweroff.playzone

import scala.reflect.ClassTag

object Test:

  class A:
    override def toString(): String = "a"
  class B extends A:
    override def toString(): String = "b"

  private def print[X, Y](from: String, x: X, xTag: ClassTag[X], y: Y, yTag: ClassTag[Y]): Unit =
    def typeof(tag: ClassTag[?]) = Option(tag).map("type " + _.runtimeClass.getSimpleName()).getOrElse("")
    println()
    println(s"$from: x = $x ${typeof(xTag)}")
    println(s"$from: y = $y ${typeof(yTag)}")

  private case class C[X](x: X):
    def func[Y <: X](y: Y)(using
        xTag: ClassTag[X],
        yTag: ClassTag[Y]
    ): Unit =
      print[X, Y]("C", x, xTag, y, yTag)

  private case class D[X](x: X)(using
      xTag: ClassTag[X]
  ):
    def func[Y <: X](y: Y)(using
        yTag: ClassTag[Y]
    ): Unit =
      print[X, Y]("D", x, xTag, y, yTag)

  private case class E[X](x: X):
    def func[Y <: X](y: Y): Unit =
      print[X, Y]("E", x, null, y, null)

  private case class F[X](x: X)(using
      xTag: ClassTag[X]
  ):
    def func[Y <: X](y: Y): Unit =
      print[X, Y]("F", x, xTag, y, null)

  def main(args: Array[String]): Unit =

    val a = A()
    val b = B()

    C(a).func(b)
    C(b).func(a)

    D(a).func(b)
    // D(b).func(a) => Does not compile

    E(a).func(b)
    E(b).func(a)

    F(a).func(b)
    // F(b).func(a) => Does not compile

Output:

C: x = a type A
C: y = b type B

C: x = b type A
C: y = a type A

D: x = a type A
D: y = b type B

E: x = a 
E: y = b 

E: x = b 
E: y = a 

F: x = a type A
F: y = b 

sangamon, how did you get this output code showing how the compiler interprets the code? Thanks.

scalac -Xprint:typer ...

See

In Scala 3 they made some changes to type inference in order to make code like this work:

List(1, 2, 3).foldLeft(Nil)((acc, i) => i :: acc)

In Scala 2 that didn’t work and you always had to go out of your way to use e.g. List.empty[Int] instead of Nil in order for this to typecheck.

So I think here you just see the other side of the medal of that same change. The code is basically the same thing since extension methods desugar into regular method calls with the receiver as an extra parameter.

2 Likes

Just to beat the drum of “v for verbose” and “w for warnings”.

-Vprint:typer -Werror

1 Like