Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write strongly typed generic extension function in Kotlin?

Tags:

Focus on the strong and generic parts.

Let's say I have this extension function:

fun <E> Collection<E>.myContains(item: E) : Boolean {
    // quite pointless, I know, but a simple example
    return item in this
}

the intention is to write a function that only accepts types of the collection elements (E), but this is not validated by the compiler?!

val isItInside: Boolean = listOf(1, 2).myContains("1")

happily compiles. My guess is that E is inferred to be Any.

How can I enforce this restriction within the Kotlin type system/generics?

(Kotlin version 1.3.41)


Original context

An exercise to try to write a small assertion framework. A bit more complicated, but tried to get the simplest repro above.

class Asserter<T>(val value: T)

infix fun <T> T.should(block: Asserter<T>.() -> Unit) =
    Asserter(this).block()

fun <T : Collection<*>> Asserter<T>.haveSize(size: Int) {
    check(this.value.size == size) {
        "expecting ${this.value} to be of size $size"
    }
}

fun <E, T : Collection<E>> Asserter<T>.contain(item: E) {
    check(item in this.value) {
        "$item does not exist in $item"
    }
}

class ShouldTest {

    @Test fun intList() {
        listOf(1, 2) should {
            haveSize(2)
            contain(1)
            contain("2") // this shouldn't compile
        }
    }

    @Test fun stringList() {
        listOf("1", "2") should {
            haveSize(2)
            contain(1) // this shouldn't compile
            contain("2")
        }
    }
}
like image 582
TWiStErRob Avatar asked Jul 26 '19 16:07

TWiStErRob


People also ask

How do I use generic Kotlin?

Generics type checks and castsif (something is List<*>) { something. forEach { println(it) } // The items are typed as `Any?` } The same syntax but with the type arguments omitted can be used for casts that do not take type arguments into account: list as ArrayList .

How do you extend in Kotlin?

In Kotlin we use a single colon character ( : ) instead of the Java extends keyword to extend a class or implement an interface. We can then create an object of type Programmer and call methods on it—either in its own class or the superclass (base class).

Where do I put Kotlin extension?

Yes, extention functions can be declared anywhere you want. For functions of general utilities (regarding lists), I whould place them under something like /company/app/util/lists. kt .


1 Answers

This appears to be due to the variance on the Collection interface's parameter, which is defined as Collection<out E>.

This means that Collection<Any> is a supertype of Collection<E>, and so (apparently) the Collection<Any>.myContains() extension can be called on a Collection<Int>.

You can confirm this by replacing it with the invariant MutableCollection (and also the listOf() with mutableListOf()); you then get the compile-time ‘Type mismatch’ error as expected.

This surprised me, though.  I guess the compiler must infer E using both the receiver type and the parameter type.  (Can anyone confirm this?)  And, as you point out, it has the annoying effect of preventing stricter type-safety.

like image 136
gidds Avatar answered Oct 21 '22 08:10

gidds