Home > front end >  Kotlin sealed interface with generic type doesn't infer constraints for it's subtypes
Kotlin sealed interface with generic type doesn't infer constraints for it's subtypes

Time:09-01

Example:

sealed interface Foo<out T> {
    val value: T
}

data class Bar<out K: List<Int>>(override val value: K): Foo<K>
    
fun <T> processFoo(foo: Foo<T>) {
    
    when (foo) {
        is Bar -> foo.value.forEach(::println)
    }
}

Fails with:

Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:

  • public inline fun <T> Iterable<TypeVariable(T)>.forEach(action: (TypeVariable(T)) -> Unit): Unit defined in kotlin.collections
  • public inline fun <K, V> Map<out TypeVariable(K), TypeVariable(V)>.forEach(action: (Map.Entry<TypeVariable(K), TypeVariable(V)>) -> Unit): Unit defined in kotlin.collections

Why this fails? I expect that if foo is of type Bar then we know that T is a subtype of List<Int>. So we should be able to call forEach on it. Am I wrong?

CodePudding user response:

This problem is simply caused by a typo in your code.

If you replace is Bar with is Bar<*>, the compiler is able to infer that T is a List<Int> in that context and the code compiles.

CodePudding user response:

I expect that if foo is of type Bar then we know that T is a subtype of List. So we should be able to call forEach on it.

Yes, that is true. But T could also implement Map<K, V> at the same time as it implements List<Int> (I don't know of such a type, but it theoretically could exist), in which case you would also be able to call this extension function:

inline fun <K, V> Map<out K, V>.forEach(
    action: (Entry<K, V>) -> Unit)

See all the different forEaches here.

To specifically call the forEach defined for Iterable, just do a cast:

// could also cast to List<Int> here - that's a completely safe unchecked cast
(foo.value as List<*>).forEach(::println)

An alternative is to use is Bar<*>, but a (very) slight drawback of this is that, as <*> projects the type of foo.value to be List<Int>, you lose the T. You won't be able to use foo.value in places where a T is expected.

A contrived example would be:

fun <T> processFoo(foo: Foo<T>): T {
    return when (foo) {
        // you can't return foo.value when you are expected to return T
        is Bar<*> -> foo.value.also { 
            it.forEach(::println) 
        }
    }
}
  • Related