Search code examples
kotlin

Why doesn't Kotlin treat numbers as "Int" by default inside sumOf function lambda?


In the below code:

val sum = listOf(1, 2, 3).sumOf { if (it % 2 == 0) 1 else 0 }

Kotlin gives the following error:

Kotlin: Overload resolution ambiguity: 
public inline fun <T> Iterable<TypeVariable(T)>.sumOf(selector: (TypeVariable(T)) -> Int): Int defined in kotlin.collections
public inline fun <T> Iterable<TypeVariable(T)>.sumOf(selector: (TypeVariable(T)) -> Long): Long defined in kotlin.collections

Playground

If I explicitly use toInt(), the error is gone but I get a warning of redundant call

val sum = listOf(1, 2, 3).sumOf { if (it % 2 == 0) 1.toInt() else 0 }

Why doesn't Kotlin automatically use Int here?


Solution

  • The spec says the following about the types of integer literals:

    A literal without the mark (L suffix) has a special integer literal type dependent on the value of the literal:

    • If the value is greater than maximum kotlin.Long value, it is an illegal integer literal and should be a compile-time error;
    • Otherwise, if the value is greater than maximum kotlin.Int value, it has type kotlin.Long;
    • Otherwise, it has an integer literal type containing all the built-in integer types guaranteed to be able to represent this value.

    So integer literals like "1" doesn't have a simple type like kotlin.Int or kotlin.Long. It has an "integer literal type".

    Example: integer literal 0x01 has value 1 and therefore has type ILT(kotlin.Byte,kotlin.Short,kotlin.Int,kotlin.Long). Integer literal 70000 has value 70000, which is not representable using types kotlin.Byte and kotlin.Short and therefore has type ILT(kotlin.Int,kotlin.Long).

    Here are the subtyping rules of these ILTs. Importantly for your question:

    ∀Ti∈{T1,…,TK}:ILT(T1,…,TK)<:Ti
    


    This rule basically says that ILTs work like an intersection type. For example, ILT(kotlin.Int,kotlin.Long) is a subtype of kotlin.Int and also a subtype of kotlin.Long.

    Now let's look at your lambda { if (it % 2 == 0) 1 else 0 }. It returns either the literal 0 or the literal 1. These both have the type:

    ILT(kotlin.Byte,kotlin.Short,kotlin.Int,kotlin.Long)
    

    which is a subtype of kotlin.Long and kotlin.Int. Therefore, your lambda can be converted to both a (T) -> Long and a (T) -> Int, in the same way that a (T) -> Dog can be converted to a (T) -> Animal.

    When you use toInt(), then only the (T) -> Int overload matches the return type, since Int is not convertible to Long implicitly.

    Apparently, if you do toInt() on the whole expression, there is no redundant toInt warning:

    fun main() {
        val sum = listOf(1, 2, 3).sumOf { (if (it % 2 == 0) 1 else 0).toInt() }
    }
    

    Also note that the compiler looks at the lambda return type only because sumOf is annotated with OverloadResolutionByLambdaReturnType. If not for this, you would still get an ambiguity error even if you use toInt(). See Using lambda return type to refine function applicability for more info.

    The reason why it is able to choose the Int overload in simple cases like:

    fun foo(x: Int) {}
    fun foo(x: Long) {}
    fun main() { foo(43) }
    

    is because of the "Choosing the most specific candidate" step in overload resolution. In this step, it handles built in numeric types differently, and considers Int the "most specific". However, this step happens just before "Using lambda return type to refine function applicability", and thinks that (T) -> Int and (T) -> Long are equally specific.