Generic type bound on extension method silently coerced to Any

To my surprise I found out I could write this in Kotlin:

class P<out T>
fun <T> P<T>.ext(e: T) {}
val x = P<String>() 

Normally, you can’t write methods parameterized by an out type, lest you end up treating a subtype of a type T as another of its subtypes:

open class A
open class B: A()
open class C: A()
class P<out T> {
    private var x: T? = null
    fun take(e: T) { x = e } // rejected by compiler
// if it were accepted:
val p: P<A> = P<B>()
p.take(C()) // oops I've written a C into a B var!

Kotlin apparently enforces the same restriction on extension methods, but instead of rejecting method ext above, it silently transforms the T bound into an Any, which is why it won’t complain when you pass an Int to a function you thought would take Strings. You can’t do anything harmful with the Int inside the body of ext: it is treated a type Any.

So at the very least, the user should be forced to write Any instead of having its bound silently coerced to Any.

But, extension methods are not regular methods, they are not dynamically dispatched. This means that you can actually never fuck up by having your ext parameter being treated as type T inside the method: the compiler already prevents these problems by forbidding real methods of out T classes to take parameters of type T.

Is there more to it than meets the eye, or should this limitation be lifted?

1 Like

I can’t say I can follow this part:

There’s no such limitation on extension functions, because they are not members. From the checker’s point of view,

fun <T> P<T>.ext(e: T)

is the same as

fun <T> ext(_this: P<T>, e: T)

Could you clarify your question?

Mighty good point. I feel dumb having overlooked that.

Okay, so followup questions.

  1. How can I get the inference mechanic for extension methods, but for member methods.

To take a concrete example, how do I translate what follows to make the extension function into a member function?

sealed class Maybe<out T>
    class Some<T>(val value: T): Maybe<T>()
    object None : Maybe<Nothing>()

inline fun <T> Maybe<T>.getOr(f: () -> T): T = when (this) {
    is Some<T> -> this.value
    is None -> f()

i.e. what I want is something like this:

sealed class Maybe<out T>
    fun <R super T> getOr(e: R): R = when (this) { // imaginary super syntax
        is Some<T> -> this.value
        is None -> e

So I want the return type of the method to be inferred to the lowest common super-type of e and the static type of the parameter T at the use site (i.e. just what the extension method does).

  1. How to put a constraint that I want a type parameter to be a subtype of the bound. So if I take back my initial example:

    class P
    fun P.ext(e: T) {}
    val x = P()

How can I make sure that this fails (because Int cannot be a subtype of String)?

To be clear, I know that this code is actually safe, I just want to add this extra restriction to catch potential errors. I also know this would be completely circumventable by assigning my P<String> to a P<Any>. I just want a more “warning” kind of check.

1 Like