Search code examples
kotlingenericsextension-function

Compile-time check for lambda type in Kotlin


I'm trying to create an extension function I can apply to collections similar to filter/map/etc. Typing due to generics is pretty loose and I want to be more strict. Is there anything I can do in Kotlin?

inline fun <T> Iterable<T>.filterIn(other: Collection<T>): List<T> {
    return this.filter { it in other }
}

inline fun <T, R> Iterable<T>.filterIn(other: Collection<R>, transform: (T) -> (R)): List<T> {
    return this.filter { transform(it) in other }
}

The following tests pass as expected:

listOf("1", "2", "3").filterIn(setOf("1", "2")) == listOf("1", "2")
listOf("1", "2", "3").filterIn(setOf(1, 2)) { it.toInt() } == listOf("1", "2")

The following tests also pass but I would ideally want them to fail at compile time:

listOf("1", "2", "3").filterIn(setOf(1, 2)) == emptyList<String>() // should know set not same type as list
listOf("1", "2", "3").filterIn(setOf(1, 2)) { it } == emptyList<String>() // should know function doesn't return same type as list
listOf("1", "2", "3").filterIn(setOf(1, 2)) { mapOf("foo" to "bar") } == emptyList<String>() // should know function doesn't return same type as list

Solution

  • I believe this is not officially supported as of Kotlin 1.9.22.

    However, there is a hidden feature that does exactly this. We can enable it by annotating type parameters with @kotlin.internal.Exact. Unfortunately, as its name suggests, it is internal and we can't use it just like that. One way to use it is:

    @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
    inline fun <T> Iterable<@kotlin.internal.Exact T>.filterIn(other: Collection<T>): List<T> {
        return this.filter { it in other }
    }
    
    @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
    inline fun <T, R> Iterable<@kotlin.internal.Exact T>.filterIn(other: Collection<R>, transform: (T) -> (@kotlin.internal.Exact R)): List<T> {
        return this.filter { transform(it) in other }
    }
    

    Of course, we don't have any guarantees it will work in future versions of Kotlin.