Dotty Documentation

0.4.0-bin-SNAPSHOT

Dropped: Weak Conformance

Edit this page on GitHub

In some situations, Scala used 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.

To simplify the underlying type theory, Dotty drops the notion of weak conformance altogether. Instead, it provides more flexibility when assigning a type to a constant expression. The new rule is:

  • If a list of expressions Es appears as one of

    • the elements of a vararg parameter, or
    • the alternatives of an if-then-else or match expression, or
    • the body and catch results of a try expression,

    and all expressions have primitive numeric types, but they do not all have the same type, then the following is attempted: Every constant expression E in Es is widened to the least primitive numeric value type equal to or above the types of all expressions in Es, if that can be done without a loss of precision. Here above and least are interpreted according to the ordering given below.

               Double
              /      \
            Long    Float
              \     /
                Int
              /    \
           Short   Char
             |
           Byte
    

    A loss of precision occurs for an Int -> Float conversion of a constant c if c.toFloat.toInt != c. For a Long -> Double conversion it occurs if c.toDouble.toLong != c.

    If these widenings lead to all widened expressions having the same type, we use the widened expressions instead of Es, otherwise we use Es unchanged.

Examples:

inline val b = 33
def f(): Int = b + 1
List(b, 33, 'a')      : List[Int]
List(b, 33, 'a', f()) : List[Int]
List(1.0f, 'a', 0)    : List[Float]
List(1.0f, 1L)        : List[Double]
List(1.0f, 1L, f())   : List[AnyVal]
List(1.0f, 1234567890): List[AnyVal]

The expression on the second-to-last line has type List[AnyVal], since widenings only affect constants. Hence, 1.0f and 1L are widened to Double, but f() still has type Int. The elements don't agree on a type after widening, hence the elements are left unchanged.

The expression on the last line has type List[AnyVal] because 1234567890 cannot be converted to a Float without a loss of precision.