Search code examples
kotlingenericstypespolymorphismsubtyping

Subclassing a generic Kotlin class with type constraint


I'm running into problems combining subclassing polymorphism and generics (ad-hoc polymorphism) in Kotlin.

Here are my type definitions:

interface State

interface StatefulContainer<out S : State> {
    val state: S
}

interface StatefulContainerRepository<C: StatefulContainer<S>, S: State>

// Specialization starts here
sealed interface MyState : State

enum class StateA : MyState { A1, A2 }
enum class StateB : MyState { B1, B2 }

sealed interface MyEntity : StatefulContainer<MyState> {
    override val state: MyState
}

data class EntityA(override val state: StateA) : MyEntity
data class EntityB(override val state: StateB) : MyEntity

sealed interface MyContainerRepository<C: StatefulContainer<S>, S: MyState>: StatefulContainerRepository<C, S>

class ARepository: MyContainerRepository<EntityA, StateA>

The type checker returns the following error: Type argument is not within its bounds. Expected: StatefulContainer<StateA>. Found: EntityA. This is odd, because EntityA is a StatefulContainer<StateA> – at least this is what I think it is. However, if I make the following modification:

data class EntityA(override val state: StateA) : MyEntity, StatefulContainer<StateA>

the type checker complains Type parameter S of 'StatefulContainer' has inconsistent values: MyState, StateA. Why is this the case? How can I correctly type the above class hierarchy?

I am used to the more straightforward ad-hoc of languages like Haskell, or pure OO subtyping. The combination here, together with the JVM's type erasure makes it hard for me to understand what is going on. So, besides the concrete question above, I would also be grateful for some more fundamental insights - which concepts are at work, that - if I manage to understand them - would help me to resolve this and similar situations?


Solution

  • This is odd, because EntityA is a StatefulContainer<StateA>.

    That's not true. EntityA is a MyEntity, and that is just a StatefulContainer<MyState>, You want it to be more specific than it actually is. Only the following will work:

    class ARepository: MyContainerRepository<EntityA, MyState>
    

    Alternatively, you can declare EntityA another way. You suggested this:

    data class EntityA(override val state: StateA) : MyEntity, StatefulContainer<StateA>
    

    This will not work either, though, because a StatefulContainer<StateA> is not compatible with a MyEntity. Their properties don't match. Let's see what would happen if the above would be allowed. To better demonstrate it, let's first make MyEntity overwrite its property as a var instead of a val (*):

    override var state: MyState
    

    Then consider this code:

    val entityA: StatefulContainer<StateA> = EntityA(StateA.A1)
    val myEntity: MyEntity = entityA
    myEntity.state = StateB.B1
    

    First, we create a new instance of EntityA, then we cast it to MyEntity. With the above declaration of EntityA that should work without any problems, as it has both types MyEntity and StatefulContainer<StateA>. The first two lines are fine. Let's then look at the last line: At first glance everything seems OK: We change the state value of a MyEntity and set it to one of the allowed subtypes. But remember, the underlying object is, in fact, an instance of EntityA. And that neither allows its value to be canged (it is declared as val), nor can it hold StateB.B1 (since it is of type StateA). Something has gone horribly wrong here. The above code is without fault, so it must be the declaration of EntityA: It obviously cannot possibly be both a MyEntity and a StatefulContainer<StateA>.

    Just remove MyEntity as a supertype of EntityA like this:

    data class EntityA(override val state: StateA) : StatefulContainer<StateA>
    

    Now your initial code will work as intended.

    (*): That's perfectly legal because by overwriting it creates a new property which can have any characteristics as long as it has also those from the super class.