Home > Software design >  Function overload ambiguous with type covariance but not ambiguous with generic
Function overload ambiguous with type covariance but not ambiguous with generic

Time:10-09

Suppose I have the following code.

open class Parent
class Child: Parent()

fun <T: Parent> foo(obj: T): T = obj
inline fun <T: Parent> foo(block: () -> T): T = foo(block())

fun <T> bar(obj: T): T = obj
inline fun <T> bar(block: () -> T): T = bar(block())

fun main() {
    /* Compile error:
    Overload resolution ambiguity. All these functions match.
        public inline fun <T : Parent> foo (block: () → TypeVariable(T)): TypeVariable(T) defined in examples in file example.kt
        public fun <T : Parent> foo (obj: TypeVariable(T)): TypeVariable(T) defined in examples in file example.kt
     */
    foo { Child() }

    // Works
    bar { "something" }
}

The first function call (foo) gives me a compilation error saying the overload resolution is ambiguous. But why is this true since the block is of type () -> T and this isn't a subtype of Parent?

Shouldn't this error occur on the second function call (bar)? Why doesn't it occur?

CodePudding user response:

My guess is that the problem here is with type inference. In each of your examples, you're relying on type inference for two things:

  1. To determine the value of the generic type parameter T, and
  2. To determine the signature of the lambda function.

In the case of foo, the overload resolution depends on knowing the value of the generic parameter T. That's because T is bounded. The actual value of T might affect whether it's valid to call this method or not, depending on whether T turns out to be a subtype of Parent.

The problem is that T needs to be inferred based on the value of the parameter being passed in, and the type of the parameter being passed in needs to be inferred based on some information about which overload has been selected and/or what the value of T is! I suspect this creates a circular dependency that prevents the overload resolution from completing.

You can fix it by providing a value either for the lambda signature or for the generic type parameter:

foo<Parent> { Child() } // works
foo({ Child() } as () -> Parent) // works

Extracting the lambda to a variable fixes it too, because it makes the compiler infer a type for the variable straight away:

val lambda = { Child() } // type is inferred as () -> Child
foo(lambda) // works, because lambda already has an inferred type

The problem doesn't arise with bar because in that case, T is unbounded. No matter what T ends up being, it won't affect whether that particular overload is allowed to be called or not. That means the compiler doesn't need to infer a value for T before performing overload resolution.

CodePudding user response:

Looks like a bug(s) in the compiler (still present in 1.6.0-M1).

Two workarounds here:

  1. Explicitly specify type argument:
foo<Child> { Child() }

Ironically, doing the same with the working part will break it (but the error won't be about ambiguity, the compiler is pretty sure what overload he's gonna call - the wrong one):

/* Compile error:
Type mismatch.
   Required: () → String
   Found: String
*/
bar<() -> String>({ "something" })
  1. Extract lambda into a separate variable:
val lambda = { Child() }
foo(lambda)

This will fix the broken part as well:

val block = { "something" }
bar<() -> String>(block)
  • Related