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
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?
CodePudding user response:
The spec says the following about the types of integer literals:
A literal without the mark 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 typekotlin.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 typeILT(kotlin.Byte,kotlin.Short,kotlin.Int,kotlin.Long)
. Integer literal 70000 has value 70000, which is not representable using typeskotlin.Byte
andkotlin.Short
and therefore has typeILT(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.