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.