5

I was reading the section 20.7 of the book Programming in Scala and I was wondering why while this code compiles:

class Food
class Fish extends Food
class Grass extends Food

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


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


val bessy: Animal = new Cow

bessy eat (new bessy.SuitableFood)

This code does not (the rest of the code is the same as before, only the last line changes):

bessy eat (new Grass)

And as far as I understand the type of Grass is the same of Cow.SuitableFood.

Also, I have another question regarding this example:

If bessy is of type Animal, how can the compiler know that it needs a type SuitableFood -> Grass instead of a type Food? 'Cause trying to provide a new Food gives me a compile error of type mismatch, but the class Animal needs a type Food and the type of bessy is explicitly defined: Animal

vicaba
  • 2,676
  • 1
  • 22
  • 40
  • Suggestion: add the tag _path-dependent-type_ to this question. That might attract an answer from someone who knows more about exactly this kind of difficulty. (I'm still struggling with path-dependent types myself.) – Ben Kovitz Aug 23 '15 at 00:23
  • @BenKovitz Added, thanks. – vicaba Aug 23 '15 at 16:39

3 Answers3

11

It's because bessie is declared Animal rather than Cow. bessie.SuitableFood is a "path-dependent type" (see below).

Try this:

val clarabelle: Cow = new Cow

clarabelle eat (new Grass)

This works because the compiler can deduce that clarabelle.SuitableFood = Grass from clarabelle's declared type.

Since bessie is declared Animal, not Cow, the compiler can't safely deduce that bessie.SuitableFood = Grass.* When you say new bessie.SuitableFood, the compiler generates code to look at the actual bessie object and generate a new instance of the appropriate type. bessie.SuitableFood is a "path-dependent type": the "path" (the bessie. part) that leads to the last identifier (SuitableFood) is actually part of the type. This enables you to have a custom version of a type for each individual object of the same class.


*Well, actually, I think that if the compiler were a little smarter, it could deduce that bessie.SuitableFood = Grass, since bessie is a val, not a var, and therefore won't change its type. In other words, the compiler ought to know that even though bessie is declared Animal, she's really a Cow. Perhaps a future version of the compiler will make use of this knowledge, and perhaps there's a good reason why that wouldn't be a good idea, which someone more expert than I will tell us. (Postscript: One just did! See Travis Brown's comment below.)

Ben Kovitz
  • 4,279
  • 1
  • 17
  • 40
  • 8
    About your footnote: if you put a type annotation on something, the compiler is going to treat it as that type, even if it _could_ infer something more specific. You'd need to use a type refinement like `val bessy: Animal { type SuitableFood = Grass }` if you wanted to keep track of the type member but not the subtype. – Travis Brown Aug 22 '15 at 22:36
1

Regarding the second part of your question: it doesn't. Animal doesn't specify that its food is Food, but some subtype of Food. Would the compiler accept this, code like your example would compile, and wrongly so. The compiler doesn't know that the necessary subtype is Grass (which is why eat(new Grass) doesn't work either), it just knows that there are some foods your cow can't eat and is cautious about it.

Silly Freak
  • 3,637
  • 1
  • 27
  • 50
1

I believe bessy eat (new bessy.SuitableFood) compiling is a bug (which was fixed in 2.11). Because another subtype of Animal could have a SuitableFood for which new makes no sense, e.g. type SuitableFood = Food or even type SuitableFood = Food with Int (Food with Int is a perfectly nice subtype of Food!).

Alexey Romanov
  • 154,018
  • 31
  • 276
  • 433