I found this in the Kotlin spec 11.4 Choosing the most specific candidate from the overload candidate set (ref):
The most specific callable can forward itself to any other callable from the overload candidate set, while the opposite is not true.
public fun <T> CompletableDeferred(parent: Job? = null): CompletableDeferred<T>
is more specific than
public fun <T> CompletableDeferred(value: T): CompletableDeferred<T>
because you can pass a Job? as a T (here unbounded, so Any?), but not vice versa.
A compile-time error would only be raised if there are multiple functions with the same specificity:
If there are several functions with this property, none of them are the most specific and an overload resolution ambiguity error should be reported by the compiler.
Most specific candidate from the oerload candidate set totally makes sense: the problem is: what type is null?
Consider this example:
class Job {}
class OtherClass {}
fun doStuff(hello: Job?): String = "a"
fun doStuff(other : OtherClass?): String = "b"
fun doStuff2(hello: Job?): String = "a"
fun <T : OtherClass> doStuff2(other : T?): String = "b"
fun main() {
// fails with ambiguous call
doStuff(null)
// What does this call?
doStuff2(null)
}
Intuitively, youâd expect the change to use generics to have similar semantics & similar behaviors; but here, we go from a compilation error to a success.
I get that arbitrary rules can be made, but my point was that a compiler is supposed to have succinct & intuitive rules.
Thatâs not really the point and canât be answered here, because there is no type context to null - the type is only then resolved after a method to call is found. When resolving the method to call, the compiler first compiles a list of all matching methods for the actual parameters given. Then it uses the most specific overload (iif exactly one exists).
Letâs step through your code.
doStuff(null)
Does doStuff(hello: Job?) match? yes (null is a valid value for Job?)
Does doStuff(other: OtherClass?) match? yes (null is a valid value for OtherClass?)
The compiler has found two matching methods. It now has to choose the most specific one. Alas, it canât! Because a Job? value canât be used as a OtherClass? value and vice versa - each method is more specific than the other. So it rightly fails with an overload resolution ambiguity compilation error.
doStuff2(null)
Does doStuff2(hello: Job?) match? yes (null is a valid value for Job?)
Does <T : OtherClass> doStuff2(other : T?) match? yes (null is a valid value for T?)
Choosing time again. Is any one method more specific than the other? Letâs try forwarding the types (using resolved type parameters here).
Can a Job? value be used as a OtherClass? value? no
Can a OtherClass? value can be used as a Job? value? no
That means, again, each method is more specific than the other - there isnât a single most specific method.
So why is there no error and the non-generic method with the Job? is chosen? Letâs check 11.4.2 Algorithm of MSC[1] selection (ref):
This check may result in one of the following outcomes:
[âŠ]
3. Both F1 and F2 are more applicable than the other.
[âŠ]
In case 3, several additional steps are performed in order.
Any non-parameterized callable is a more specific candidate than any parameterized callable [âŠ]
We see: as a tie-breaker between a non-generic and a generic method, the non-generic method wins (is more specific) - which makes sense, because itâs kinda in the name âgenericâ that this means itâs more vague.
A formal language specification hardly counts as âarbitrary rulesâ, itâs well-defined so the compiler etc. can be implemented to obey it. Whether the spec is âsuccint & intuitiveâ is open for debate, but it really doesnât need to be - in fact it canât, because it has to cover all the edge-cases so no ambiguities arise.
Well-defined & arbitrary are orthogonal; something can be well-defined & still be arbitrary, which seems to be the case here.
My concern is that in the case of CompletableDeferred, this results in unclear/poor behavior (a completed vs incomplete Deferred), and âsuccinct & intuitiveâ certainly should be a goal of a well-defined compiler
Can be, yes. My point is, that the rules in the spec are not âbased on random choice or personal whim, rather than any reason or systemâ (the definition of arbitrary). They were made consciously - in case of multiple possible choices, weighing them against each other exploring pros and cons of each.
That is a bit unfortunate, true. Itâs not the fault of the compiler or the spec though, but rather of the creator of that API. Granted, some suport from the compiler / IDE would have been nice when defining such a construct, but it canât catch everything or be too restrictive.
Ah, okay, I thought it gets a ârealâ type after the overload is chosen. For reference, The Nothing type says:
The nullable variant of this type, Nothing?, has exactly one possible value, which is null. If you use null to initialize a value of an inferred type and thereâs no other information that can be used to determine a more specific type, the compiler will infer the Nothing? type:
val x = null // 'x' has type `Nothing?`
val l = listOf(null) // 'l' has type `List<Nothing?>`
Still not so sure about it, as the article talks about the type of the variables / type parameters here. Also, there is âother information that can be used to determine a more specific typeâ after choosing an overload.
So Iâm wondering if there isnât a bit of cyclic logic going here which is why Iâm worried the choice the compiler is making here is not the right one.
What I mean is: 11.4.2 Algorithm of MSC only applies here if null is actually a Job? - i.e. itâs clear that if we first typecast the null to a OtherClass?, that it will in fact call the generic version
So the problem seems to be that null is not being processed as Nothing? but rather a Job? - is there a section in the spec which says that null resolution should happen after overload method selection?
That seems like cyclic logic, i.e. seems like null resolution should happen before method selection, which would lead to an error here (because the null can be resolved to 2 different methods, thus ambiguous)
There is no ânull resolutionâ happening here. The compiler does not ask the question âIs this null a Job? or an Int?â. It canât know the answer to that question. Instead it uses the types to decide which function to call.
In your case Nothing? is a subtype of both Job? and Any? (which is Tâs upper bound). And since Job? is more specific it chooses that one.