Understanding Scala use of abstract types and Classes

146 Views Asked by At

The next code does not compile. Why can't Santorin eat HorseFood? Tornado is declared as a new Horse, and Horse is a subtype of Animal, yet it can "eat" HorseFood.

import scala.io.StdIn._
import util._


class Food

abstract class Animal
{
    type SuitableFood <: Food
    def eat(food: SuitableFood)
}

class Grass extends Food  /*extends meaning should be "more specific than*/
class Fish extends Food
class HorseFood extends Grass

class Cow extends Animal
{
    type SuitableFood = Grass
    override def eat(food: SuitableFood) = {}
}

class Horse extends Animal
{
  type SuitableFood = Grass
  override def eat(food: SuitableFood) = {}
}

class Whale extends Animal
{
    type SuitableFood = Fish
    override def eat(food: SuitableFood) = {}

}

object abstractExample1 {

  def main(args: Array[String]) {

    val bessy: Animal = new Cow
    println("bessy = " + bessy)
    bessy eat (new Grass).asInstanceOf[bessy.SuitableFood]
     /*added this line because of a great answer someone posted for this questions but he deleted*/

    val willie: Animal = new Whale
    println("willie = " + willie)

    val santorin: Animal = new Horse
    println("santorin = " + santorin)

    val tornado = new Horse
    tornado eat new HorseFood

    print("tornado = " + tornado)
    santorin.eat(new HorseFood)

  }

}

Shouldn't this be automatically be allowed (as Horse extends Animal)? Why isn't it?

Notice that Tornado, as declared can eat HorseFood that extends Grass, and that the food parameter at the class Horse is Grass.

Is there a problem here with the = ? I mean, SuitableFood is exactly Grass and not a class C extension of Grass.

3

There are 3 best solutions below

3
Sachin On BEST ANSWER

Santorin is an Animal not a Horse so it 'eats' 'type' not HorseFood. This is because reference to it is for Animal. Change val santorin: Animal = new Horse to val santorin: Horse = new Horse and it would work fine

2
Bogdan Vakulenko On

Animal#SuitableFood is subtype of Food

implicitly[Animal#SuitableFood <:< Food]

Each instance of Horse assigned to Animal-typed val contains brand new SuitableFood type which is subtype of Animal#SuitableFood

val santorin: Animal = new Horse
val santorin2: Animal = new Horse

implicitly[santorin.SuitableFood <:< Animal#SuitableFood]
implicitly[santorin2.SuitableFood <:< Animal#SuitableFood]
implicitly[santorin.SuitableFood =:= santorin2.SuitableFood] //-- fails
//                     ^^^^^^^                    ^^^^^^^
//                       these types are different
implicitly[santorin.SuitableFood =:= Grass] // -- fails

val tornado = new Horse
val tornado2 = new Horse

implicitly[tornado.SuitableFood =:= tornado2.SuitableFood] // compiles!
//                     ^^^^^^^                    ^^^^^^^
//                               Grass =:= Grass

implicitly[tornado.SuitableFood =:= Grass] // compiles

Signature of eat method for santorin: Animal will be the following:

def eat(food: santorin.SuitableFood) // SuitableFood is path-dependent

santorin.SuitableFood is a subtype of both Food and Animal#SuitableFood

HorseFood is subtype of Food and Grass but not subtype of santorin.SuitableFood or Animal#SuitableFood

implicitly[HorseFood <:< Food]
implicitly[HorseFood <:< Grass]
implicitly[HorseFood <:< santorin.SuitableFood]//  -- fails
implicitly[HorseFood <:< Animal#SuitableFood]//  -- fails

That's why calling method eat on HorseFood is failing as well. Because HorseFood is not subtype of santorin.SuitableFood.

And that's why following works:

santorin.eat((new HorseFood).asInstanceOf[santorin.SuitableFood])
3
Aleksey Isachenkov On

You should read about variance.

Your classes are invariant. It means Animal with SuitableFood <: Food can't be an Animal with SuitableFood = HorseFood.

You can see it if you rewrite your classes to generic-like style:

trait Food
class Grass extends Food
class HorseFood extends Grass

abstract class Animal[SuitableFood <: Food] {
    def eat(food: SuitableFood)
}

val animal1: Animal[HorseFood] = null
//the next line doesn't compile because Animal[HorseFood] isn't a subclass of Animal[Food]
val animal2: Animal[Food] = animal1

So an animal who can eat Food can't eat HorseFood.