Consider the following interfaces in a module defining a protocol:
sealed interface Animal {
val name: String
}
interface Cat : Animal {
fun meow()
}
interface Dog : Animal {
fun bark()
}
Then one needs to implement a Cat and a Dog in a separate module, so we may have something like:
data class MyCat(override val name: String) : Cat {
override fun meow() {
println("$name: meow!")
}
}
data class MyDog(override val name: String) : Dog {
override fun bark() {
println("$name: bark!")
}
}
Did you notice? We actually want to preserve a specific format for logging: the name of the animal, followed by colon, followed by a description of the noise they make. What if we wanted to share part of the implementation?
Sure, you might say â we can use delegated interface implementation. But it would be difficult to share state that way, so itâs not suitable for every case. And itâs more costly, as well â for it requires an additional instance to be constructed.
So, we could do the following:
abstract class MyAnimal internal constructor() : Animal {
protected fun makeNoise(description: String) {
println("$name: $description")
}
}
data class MyCat(override val name: String) : MyAnimal(), Cat {
override fun meow() {
makeNoise(description = "meow!")
}
}
data class MyDog(override val name: String) : MyAnimal(), Dog {
override fun bark() {
makeNoise(description = "bark!")
}
}
The only problem is⊠Well, we canât do that because the Animal interface is sealed and as such canât be implemented from within another module (or even just another package). The inability of MyAnimal to implement Animal results in what Iâd call our unavoidable inconsistency of the taxonomy.
But if Kotlin allowed sealed interfaces to be implemented by abstract classes (with an internal constructor[1]) or any sealed classes, without adding those implementations to the sealed taxonomy[2] of the interfaces, it wouldnât be of any issue, as long as the language imposes concrete[3] implementations of those classes to implement one of the interfaces inheriting from the sealed one.
(NOTE: I elaborated the necessary rules better in this post.)
In our example,
MyAnimalis allowed to implementAnimalbecauseMyAnimalis abstract;MyCatandMyDogare allowed to inherit fromMyAnimalbecause they also implementCatorDog, which are subtypes in the sealed taxonomy[2] of the sealedAnimalinterface.
In conclusion,
- literally any protocol that we wish to separate from its implementation may benefit from the proposed changes;
- the proposed changes are not breaking changes and wonât harm backwards compatibility;
- the taxonomy of sealed classes and sealed interfaces is already only enforced on compile-time, so an implementation of the changes would probably be quite straightforward, as it doesnât affect code generation, only code validation.
ADDENDUM: Among the various reasons why MyAnimal should implement Animal:
- thatâs logically the most sensible thing (i.e. my animals are still animals);
- we shouldnât need workarounds (such as those suggested by various users), in order to do something that is logical, sound, and does in no way defeat the purpose of sealed interfaces;
- in this post I elaborated a practical example showing how necessary it can be to be able to upcast
MyAnimaltoAnimal.
Notes:
- The abstract class needs to have an internal constructor or be sealed altogether, since the compiler needs to track subclasses so it can enforce the rule I specified for concrete[3] implementations. This applies recursively to any abstract subclasses and abstract subclasses of abstract subclasses.
- By (sealed) taxonomy, I mean the ârestricted class hierarchyâ of (direct or indirect) subclasses of a sealed type, as in the first paragraph here.
- By concrete, Iâm referring to any non-abstract class, including ones that are
open.