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,
-
MyAnimal
is allowed to implementAnimal
becauseMyAnimal
is abstract; -
MyCat
andMyDog
are allowed to inherit fromMyAnimal
because they also implementCat
orDog
, which are subtypes in the sealed taxonomy[2] of the sealedAnimal
interface.
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
MyAnimal
toAnimal
.
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
.