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?
This is odd, because
EntityA
is aStatefulContainer<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.