No error for ambiguous method call?

val test2 = CompletableDeferred<Int?>(value = null)
println(test2.isActive)
println(test2.await())

Calls the CompletableDeferred(value: T) overload

val test2 = CompletableDeferred<Int?>(parent = null)
println(test2.isActive)
println(test2.await())

Calls the CompletableDeferred(parent: Job? = null) overload

I would expect:

val test2 = CompletableDeferred<Int?>(null)

To be an ambiguous method call resulting in a compile-time error; however, it seems to call CompletableDeferred(parent: Job? = null)

Am I missing something, or is this a compiler bug?

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.

I guess that makes sense. :slight_smile:

2 Likes

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.

1 Like

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. :foot:

doStuff(null)

  • Does doStuff(hello: Job?) match? :arrow_right: yes (null is a valid value for Job?)
  • Does doStuff(other: OtherClass?) match? :arrow_right: 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? :arrow_right: yes (null is a valid value for Job?)
  • Does <T : OtherClass> doStuff2(other : T?) match? :arrow_right: 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? :arrow_right: no
  • Can a OtherClass? value can be used as a Job? value? :arrow_right: 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.


  1. most specific candidate ↩︎

1 Like

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

While you are correct that that’s not the point, it can be answered here: null is of type Nothing?.

1 Like

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.