Question

I have read a few questions on the SO and elsewhere and still do not understand well where this "widening" of a parameter type can be helpful, i.e. compliying to Liskov substitution principle. The following code I took from an answer on the SO, explaining contravariance:

//Contravariance of parameter types: OK
class Food
class FastFood extends Food

class Person { eat(FastFood food) }
class FatPerson extends Person { eat(Food food) }

So I understand that the overridden method accepts more generic paramater than the method in its ancestor. But in practice, how does this help? I mean, if the original method works with certain properties of the derived type, none of this will be available in the derivative using the supertype of the parameter. Therefore, I might have issues with fulfilling the contract postconditions, if those relate to the subtype somehow. Like:

class Animal {}
class Cat { void Meow() void CatSpecificThing()}
...

class A
{
   List<Cat> ListOfCats;
   void X(Cat c)
   {
      c.Meow()
      c.CatSpecificThings()
      ListOfCats.Add(c)

   }
}
class B : A
{
   void X(Animal a)
   {
       //how is this now useful? I cannot do anything that needed Cat
   }
}

Let's say the postcondition of X method is to update the ListOfCats. But in the overriden method in the derived class, I would not be able to do it if there was just the supertype..?

I would be extremely happy for a simple example that demonstrates how this is useful.

Was it helpful?

Solution

I think one point where you are possibly getting confused is that contravariance includes the case where the types are equal. It isn't a Liskov violation to use the same type in the arguments of the derived method, so in your example it's perfectly okay to use a Cat instead of an Animal in B. The vast majority of real-world use cases will use the exact same class rather than a wider one.

As an example of where contravariance is useful, consider the following Scala example:

class Pet
class Cat extends Pet
class Kitten extends Cat

class Cats(cats: Cat*) {
  def sorted[B >: Cat](ordering: math.Ordering[B]): Seq[Cat] =
    cats.sorted(ordering)
}

class Kittens(kittens: Kitten*) extends Cats {
  override def sorted[B >: Kitten](ordering: math.Ordering[B]): Seq[Kitten] =
    kittens.sorted(ordering)
}

In Scala, >: is how you specify contravariance. Here, you can sort Kittens using an Ordering[Kitten], but you can also use an Ordering[Cat] or an Ordering[Pet], which makes sense. Cats can only be sorted using an Ordering[Cat], or an Ordering[Pet], so their ordering argument is narrower, it can accept fewer types. It wouldn't make sense to sort Cats by an Ordering[Kitten].

What if we narrowed the argument instead by only accepting Ordering[Kitten] in the sorted for Kittens? That would make the following code fail to type check:

val cats: Cats = new Kittens(new Kitten, new Kitten)
val ordering: math.Ordering[Cat] = math.Ordering.by(_.toString)
cats.sorted(ordering)

This wouldn't type check because the last line wants to call the sorted in class Kittens, but that method doesn't allow an Ordering[Cat]. That failure to type check is why narrowing the method arguments in a subclass violates the Liskov Substitution Principle.

OTHER TIPS

In this case it seems useless, but microsoft docs itself gives quite a useful case:

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

In this case you are able to assign a more generic parametered action to a more derived one. Why is this useful? your base class action might already exist, this way you don't have to wrap it into another (lambda) function to create the desired: Action<Derived>

Instead of saying I have this interface why do i need this base class method. Turn your question around: I have this Base class with this method (which is already used), I want to implement the interface. without contravariance you would specifically have to add an extra method.

Other reasons could be that: Derived/Cat class is in an unreachable dependency but de base class is reachable. By being able to use the baseclass your code is less coupled and just as specific as it needs to be (more re-useable).

Licensed under: CC-BY-SA with attribution
scroll top