I talked about covariance several times in the past, particularly in this post about covariant return types. Since then, C# added support for this feature, and obviously Kotlin comes with it since the beginning. I'll paste below the wikipedia definition just in case:
a covariant return type of a method is one that can be replaced by a "narrower" (derived) type when the method is overridden in a subclass.
Covariant return types (or return type covariance) makes perfect sense, it does not go against any programming principle (Liskov...) and it seems the only reasons for not supporting it were technical limitations in runtimes, compilers...
On the other hand we have Covariant method parameter type. This is a very different story:
To allow the parameters of an overriding method to have a more specific type than the method in the superclass
In principle it seems perfectly logical, as logical as covariant return types, but indeed it's terribly problematic and for the most part not a good idea and that's why mostly no language supports it, save for Eiffel and Dart. The wikipedia article explains it pretty good. As far as I know Python type checkers do not support it either (not even with some optional parameter). I've recently had a real use case at work where I found myself wanting this feature (and not having it), that's why I've been reminded about it and its problems, and I'm talking about it today.
The thing is that such a feature makes perfect sense in terms of modeling the world, but opens the door to runtime errors in our code. If you have an Animal class with an eat(food) method that receives Food, it seems perfectly normal to model a situation where Cat.eat wants to restrict it's food parameter to CatFood. The problem is that then, if we have a variable typed as Animal that is pointing to a Cat, and we provide it Food, rather than CatFood, that will crash at runtime (if our cat is using some CatFood method that is not present in Food). Basically, replacing an Animal by Cat is no longer save, we're going against the Liskov Substitution Principle. To overcome the lack of this feature you can document that your method expects CatFood rather than Food, and use a cast inside it, but as the compiler is not enforcing it, you can go through runtime errors, the difference is that you will be aware of the very specific places where the errors can take place (those where you're doing a casting). I mean (using Kotlin for the example):
// Food hierarchy
open class Food(val name: String)
class CatFood : Food("Cat Food") {
fun addFishContent(){}
}
open class Animal {
open fun eat(food: Food) {
println("Animal eats ${food.name}")
}
}
class Cat : Animal() {
override fun eat(food: Food) {
// I've documented that I should be provided CatFood, not just Food
// if they've not done I'm fully aware this will CRASH
(food as CatFood).addFishContent()
}
}
The above approach seems reasonable, we have a well delimited potential error source (the casting), while error possibilities with "Covariant Parameter type" totally go out of control. Anywhere you use an instance of an "inheritable" class would become a potential error source. Maybe you think you are save because you have not defined any child class with a method with a "covariant parameter types" but anyone could write one in the future, so your code is not safe.
No comments:
Post a Comment