Search code examples
scalainheritancecovariancecontravariancesubtyping

Use super method with sub type param


I'm trying to implement some function in a super class so I don't have to always repeat it in its children. Sample:

trait Animal {
  def applyF(transition: Animal => Animal): Animal = transition(this) // Animal as param and return type
}
case class Cat(color: String) extends Animal {
  def changeColor(color: String): Cat = this.copy(color)

  def update(): Animal = {
    val transition = (cat: Cat) => cat.changeColor("yellow") // Cat as param and return type
    applyF(transition) // <-- Type mismatch, expected: Animal => Animal, actual: Cat => Cat
  }
}

But this gives a type mismatch because Cat is not Animal. Why does this not work? Cat extends Animal so it should be an animal right?

Has this something to do with co-/contravariant?

How can I fix it?

----- Update -----

Second example:

trait Animal {
  def applyF[A >: this.type <: Animal](transitions: Iterable[A => Animal]): Animal =
    transitions.foldLeft(this)((animal, transition) => transition(animal))
}
case class Cat(color: String) extends Animal {
  def changeColor(color: String): Cat = this.copy(color)

  def update(): Animal = {
    val transition = (cat: Cat) => cat.changeColor("yellow") // Cat as param and return type
    applyF(Iterable(transition)) // <-- Type mismatch, expected: A, actual: entity.type (with underlying type example.state.Entity)
  }
}

Solution

  • Cat extends Animal but Cat => Cat doesn't extend Animal => Animal.

    A => B is covariant with respect to B and contravariant with respect to A, i.e. if A1 <: A, B1 <: B then A => B1 <: A => B <: A1 => B.

    What if you parametrize Animal#applyF?

    trait Animal {
      def applyF[A >: this.type <: Animal](transition: A => Animal): Animal = transition(this)
    }
    

    trait Animal { 
      def applyF[A >: this.type <: Animal](transitions: Iterable[A => A]): Animal /*A*/ =
        transitions.foldLeft[A](this)((animal, transition) => transition(animal)) 
    }