Difference in Scala 2 and 3 when inferring narrowed type in subclass

Scala 2:

scala> trait A { def x: Any }
trait A

scala> class B extends A { def x = "x" }
class B

scala> new B().x
val res0: String = x

Scala 3:

scala> trait A { def x: Any }
// defined trait A

scala> class B extends A { def x = "x" }
// defined class B

scala> new B().x
val res0: Any = x

So it seems that in Scala 2 the type of x is inferred as String, but in Scala 3 it is kept as Any.

What is the reason for this?

see Type Inference | Scala 3 Migration Guide | Scala Documentation

1 Like

I guess there is some good reason then. Too bad.

It’s not that big of a hassle to write:

class B extends A { def x: String = "x" }

But when you have something like this, it becomes mildly unpleasant:

class Wrapper[T](x: T)

trait A:
  def tup: Tuple

class B extends A:
  val a = Wrapper("foo")
  val b = Wrapper(1)
  val c = Wrapper("bar")
  val d = Wrapper(true)  
  def tup: (Wrapper[String], Wrapper[Int], Wrapper[String], Wrapper[Boolean]) = (a, b, c, d)

A solution could be:

  val temp = (a, b, c, d)
  def tup: temp.type = temp

But it’s a bit ugly.

The Scala 2 ticket was finally closed as out of scope.

The common examples are

def f: Option[A]
override def f = None

default void remove() { throw new UnsupportedOperationException(); }
override def remove() = throw new UnsupportedOperationException

or override def remove() = ???

I wonder if there is a Scalafix lint for “inferred type of public member”, as several comments insisted it is a sin.

The Java example suggests that a minimal change would be not to narrow to inferred bottom types when overriding Java API.

Also maybe infer narrowed type if the method is effectively final; I don’t know if there are benefits when inlining. In Scala 3 there is transparent.

The use case sounds like derivation or generation instead of inference.

Could we have e.g. an @infer annotation for this?