How does Generics Specialization work in Kotlin?

I found myself writing some code that looked like this:

fun foo(x: Int) = // special case
fun <T> foo(x: T) = // general case

… and I thought “there’s no way that will compile”. I’ve written Rust in the past and have bashed my head against this sort of thing before, but convinced myself that it is probably for the best that this doesn’t compile in Rust. After all, which function should the compiler call for foo(1)?

But then it did compile. So my question is: what are those rules? Where are they documented? Is it good practise to rely on these rules?

And a follow up observation: the rules currently seem to be sensible, and what you would expect. I did some testing:

interface MyInterface { }
open class MySuperclass()
data class MyClass<out E>(val x: E) : MySuperclass(), MyInterface

// Test different specificity
fun foo(x: MyClass<String>) = 1  // Most specific case
@JvmName("foo2")  // Needs separate name because same JVM signature as #1
fun foo(x: MyClass<Any>) = 2  // E is covariant, so MyClass<E> is a subclass of MyClass<Any> for all E
@JvmName("foo3")  // Needs separate name because same JVM signature as #1
fun <E> foo(x: MyClass<E>) = 3
fun foo(x: MySuperclass) = 4
fun foo(x: MyInterface) = 5
fun <T> foo(x: T) = 6 // Most general case

fun main() {
    val x = MyClass("test")
    println(foo(x))  // Prints 1
    println(foo(x as MyClass<Any>))  // Prints 2
    println(foo(x as MyClass<Any?>))  // Prints 3 - equivalently one can write `as MyClass<*>`
    println(foo(x as MySuperclass))  // Prints 4
    println(foo(x as MyInterface)) // Prints 5
    println(foo(x as Any?)) // Prints 6
}

I also observed that fun <T: SomeType> foo(x: T) is identical to fun foo(x: SomeType). This is not true for example in Rust where the latter uses dynamic dispatch but the former creates a copy of the function for each concrete type (monomorphization). In Kotlin we always use dynamic dispatch (I think). Interestingly, these functions are so identical in Kotlin that they seem to have the same JVM signature, so you would need the @JvmName annotation to list both.

2 Likes

These rules are actually clearly documented in the Official Kotlin language spec on overload resolution

2 Likes

Ah right, thanks. I didn’t think to look to the specification, which I guess is foolish of me. I’ve only needed their website documentation up until this point in time.

1 Like

The spec has only been recently made public so it’s actually not that well known. Besides like it requires a lot of knowledge of mathematical jargon to be able to understand it so most people would instead fall back to asking online

1 Like

This would contradict:

In case 2, an additional step is taken: a non-parameterized callable is a more specific candidate than any parameterized callable. If this step does not allow for deciding the more specific candidate, this is an overload ambiguity which must be reported as a compile-time error.

But you may refer to clashing of both variants when not disambiguating both by @JvmName, that’s true because generics decay to their upper bound in the JVM resulting into both signatures being the same.

No, the latter doesn’t use dynamic dispatch in Rust unless you call some method of x in foo, then dynamic dispatch is used for x but not for foo itself.

And yes, the former would generate a new (monomorphized) method in the backend in case of Rust while the same method is called in the JVM with additional casts afterwards.

Personally, I find neither variant superior, rather I would appreciate the compiler/user choosing the right strategy for the actual case.

In Kotlin we always use dynamic dispatch (I think).

Only for methods. Free/extension/companion object functions are static though. Though I would like to have overriding extension functions as well.

2 Likes

Mostly true, althoug members of companion objects are resolved dynamically as well. The compiler will generate a static acess to get the instance of the companion object. After that it is treated as any other instance in kotlin. If you want static dispatch for companion object members you have to annotate them wiht @JvmStatic (I don’t think there is a JS or native equivalent).

2 Likes