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.

1 Like

No. All the compiler knows about that static null in your code is that it’s of type Nothing? - it doesn’t get a more concrete type than that. The compiler then compiles[1] a list of all matching overloads to call (as @ebrowne72 said, Nothing? fits both Job? and Any?). Then the MSC is chosen, if there is one. All the while the type of null stays at Nothing?.


You probably know that, just mentioning it for other readers:

If you want to force a specific overload, you can assign that null to an explicitly typed variable so it’s type is not Nothing?:

val j: Job? = null
val o: OtherClass? = null

doStuff2(j)
doStuff2(o)

Although, in my opinion, it’s bad API design, if I need to do this.

Another possibility is to use named parameters, if the parameter names are different (this can only be used with Kotlin functions, though):

doStuff(hello = null)
doStuff(other = null)

  1. pun intended ↩

1 Like

That makes sense with it staying Nothing?.

I guess the real issue is simply the Any non-parameterized callable is a more specific candidate than any parameterized callable - seems like such a tie breaker has little gain & is more likely to cause bugs than simply popping up a compiler error indicating the call is not intuitive (or, at a minimum, a warning that can be turned into an error via compiler args)

Compile warnings, afterall, are meant for the developer & not the compiler (the compiler can obviously proceed)

Another way would be

doStuff(null as OtherClass?)

Understood there are workarounds, but compiler warnings having the intent of guiding users to code that is more succinct & less error-prone, this seems like it should certainly qualify

Interestingly, C++ seems to also have this, which I’m guessing was just carried over to Java and then to Kotlin

One could just as easily have made the tie-break rule well-defined by making the tie breaker “alphabetical based on the matching types”, eliminating more classes of ambiguous calls.

It seems like “is it a generic/templatized match” as a tie breaker may simply have been a bad call & it would’ve been best to not have it as such

By the way, I want to add that kotlinx.coroutines can easily resolve that ambiguity by adding an overload of the form:

public fun <T> CompletableDeferred(parent: Nothing?): CompletableDeferred<T> 

and so this ambiguity is a library design issue and not a language issue. It’s a gotcha for library designers, but normal users will likely never run into this ambiguity because it’s at the intersection of Kotlin features that practically only libraries use.

I’m an old-school developer (first C++, then Java, now Kotlin) with 25 years of coding & I’m only now realizing this, so I think it’s something that’s really easy to miss when you’re designing a library.

I think a compiler warning would be the right way to guide library designers that this may be a poor decision

Filed a FR @ https://youtrack.jetbrains.com/issue/KT-63998