kotlintype-inference

Type inference of heterogeneous collections


Can someone please explain why res2 in the REPL session shown below is inferred as List<Any>? I thought since both Int and Char are Serializable (evidenced by res0 and res1), then res2 should also be list<Serializable>. What is different about res2?

[0] listOf('A', Pair('X', 'Y'))
res0: List<java.io.Serializable> = [A, (X, Y)]
[1] listOf(1, Pair('X', 'Y'))
res1: List<java.io.Serializable> = [1, (X, Y)]
[2] listOf(1, 'A')
res2: List<Any> = [1, A] 

Solution

  • The type of listOf(1, 'A') should be a List of an intersection type between Comparable<*> and Serializable. After all, Int and Char are also Comparable.

    fun main() {
        val x = listOf(1, 'A')
        // both of these compile
        f(x)
        g(x)
    }
    
    fun f(x: List<Serializable>) {}
    fun g(x: List<Comparable<*>>) {}
    

    It is important that the x above is declared in a statement scope (See scopes), in the body of main. If it had been declared in a declaration scope (such as the top level),

    val x = listOf(1, 'A') // this is now in a declaration scope
    fun main() {
        // now none of these compile!
        f(x)
        g(x)
    }
    
    fun f(x: List<Serializable>) {}
    fun g(x: List<Comparable<*>>) {}
    

    This is because the type of a non-local property like x cannot contain non-denotable types (e.g. intersection types), and so type approximation is applied. Type approximation replaces the intersection type with the GLB of Serializable and Comparable<*> , which is Any.

    Something similar is happening in the REPL. You can think of the expressions you enter into the REPL as being assigned to global properties called res0, res1, etc. The type that the REPL produces as output is kind of like printing ::res0.returnType. There is no KType that can represent a intersection type, so the intersection type must be approximated.


    Even if you write val x = listOf(1, 'A'), the type of x will still be List<Any>, because the REPL treats this declaration similar to a top-level declaration like in the code snippet above. This can be shown by the fact that smart casts are not possible on top level vars:

    >>> var x: Int? = null
    >>> if (x != null) { println(x.toLong()) }
    error: smart cast to 'Int' is impossible, because 'x' is a mutable property that could have been changed by this time
    if (x != null) { println(x.toLong()) }