Hi Scala Community!
This thread is the SIP Committee’s request for comments on a proposal to remove Weak Conformance from the language. You can find all the details here.
Summary
In some situations, Scala uses a weak conformance relation when testing type compatibility or computing the least upper bound of a set of types. The principal motivation behind weak conformance was to make an expression like this have type List[Double]
:
List(1.0, math.sqrt(3.0), 0, -3.3) // : List[Double]
It’s “obvious” that this should be a List[Double]
. However, without some special provision, the least upper bound of the lists’s element types (Double, Double, Int, Double)
would be AnyVal
, hence the list expression would be given type List[AnyVal]
.
A less obvious example is the following one, which was also typed as a List[Double]
, using the weak conformance relation.
val n: Int = 3
val c: Char = 'X'
val n: Double = math.sqrt(3.0)
List(n, c, d) // used to be: List[Double], now: List[AnyVal]
Here, it is less clear why the type should be widened to List[Double]
, a List[AnyVal]
seems to be an equally valid – and more principled – choice.
Weak conformance applies to all “numeric” types (including Char
), and independently of whether the expressions are literals or not. However, in hindsight, the only intended use case is for integer literals to be adapted to the type of the other expressions. Other types of numerics have an explicit type annotation embedded in their syntax (f
, d
, .
, L
or '
for Char
s) which ensures that their author really meant them to have that specific type).
Therefore, we propose to drop the general notion of weak conformance, and instead keep only one rule: Int
literals (only) are adapted to other numeric types if necessary. This rule yields the following results as examples:
inline val b = 33
def f(): Int = b + 1
Array(b, 33, 5.5) : Array[Double] // b is an inline val
Array(f(), 33, 5.5) : Array[AnyVal] // f() is not a constant
Array(5, 11L) : Array[Long]
Array(5, 11L, 5.5) : Array[AnyVal] // Long and Double found
Array(1.0f, 2) : Array[Float]
Array(1.0f, 1234567890): Array[AnyVal] // loss of precision
Array(b, 33, 'a') : Array[Char]
Array(5.toByte, 11) : Array[Byte]
Implications
The changes in weak conformance mostly change the inferred type of some expressions, typically from a precise numeric type such as Double
to AnyVal
. In most cases, the new inferred type will result in a type error soon after the given expression, which can be fixed by using explicit calls to .toDouble
(or similar) on the subexpressions that the user wants to be converted.
In some cases, it is possible that the new code does not trigger a compile error, and might subtly change some behavior at run-time. For example, the following snippet:
def sameClass[T](xs: T*): Boolean = xs.tail.forall(_.getClass == xs.head.getClass)
val x: Int = 5
sameClass(x, 5.5)
will compile before and after the change, but will display true
in Scala 2 and false
in Scala 3.
Opening this Proposal for discussion by the community to get a wider perspective and use cases.