Reputation: 3868
Considering Bulldog:
trait Animal {
type Food
def defaultFood(): Food
}
class Bulldog extends Animal {
type Food = Steak
... implementations ...
}
The function Bulldog.defaultFood()
works just fine for compiler (though my syntax highlighter gave an error, this is no big deal):
val bulldog = new Bulldog()
val df: bulldog.Food = bulldog.defaultFood()
However if bulldog is enclosed inside another class, all hell break loose:
class Kennel(val animal: Animal) {
}
def getSupply(kennel: Kennel): kennel.animal.Food = {
... implementation ...
}
val kennel = new Kennel(bulldog)
val df2: bulldog.Food = getSupply(kennel)
Scala compiler will throw a compile error:
type mismatch;
found : Option[kennel.animal.V] where val kennel: Kennel
required: bulldog.Food
Is this feature currently missing in Scala? Is there any way to make it work?
Upvotes: 1
Views: 299
Reputation: 11637
Parametric polymorphism (i.e., typeclasses) is a better way to model this. The main operation here is that you have an animal and you have to retrieve an instance of its favourite food. Turn this into a typeclass and provide instances for specific animals and their favourite foods. E.g.:
@annotation.implicitNotFound(
"Couldn't confirm that ${Animal}'s favourite food is ${Food}")
trait DefaultFood[Animal, Food] { def apply(animal: Animal): Food }
object DefaultFood {
/** Helper to easily implement typeclass instances. */
class Impl[Animal, Food](getFood: Animal => Food)
extends DefaultFood[Animal, Food] {
override def apply(animal: Animal): Food = getFood(animal)
}
}
class Bulldog
object Bulldog {
type Steak = String // Or whatever.
implicit val defaultFood: DefaultFood[Bulldog, Steak] =
new DefaultFood.Impl(_ => "Steak")
}
class Kennel[Animal, Food](
animal: Animal)(implicit defaultFood: DefaultFood[Animal, Food]) {
def getSupply: Food = defaultFood(animal)
}
object Test {
val kennel = new Kennel(new Bulldog) // Kennel[Bulldog, Bulldog.Steak]
}
This propagates the generic types forward into the Kennel
class, but
because of Scala's type inference, you don't actually have to spell out
the types. Also the @implicitNotFound
annotation gives you a nice
compile error message if there's no typeclass instance for a particular
animal and its food.
Note that the Animal
and Food
types don't actually need to be
animals and foods; that's just the semantics we're using here. If you
look at it in the most generic possible way, this typeclass is actually
just a function of type A => B
for some A
and B
.
Upvotes: 0
Reputation: 9698
Your code has a compilation problem - kennel.animal
is unresolvable since class Kennel
doesn't expose animal
as a public field; this is easily resolved by adding val
in front of animal
.
What seems to be bothering you though is that Kennel
has no knowledge about the underlying animal, other than it's an Animal
. Passing a bulldog is seen as passing some animal.
Try adding some more type info to Kennel
:
class Kennel[A <: Animal](val animal: A) { }
def getSupply[A <: Animal](kennel: Kennel[A]): kennel.animal.Food
val kennel = new Kennel(bulldog)
val df2: bulldog.Food = getSupply(kennel)
Now Kennel
knows the exact type of animal
(e.g. Bulldog
) and getSupply
is able to return the exact type of food for that animal.
Here's full working code that you can try out:
trait Steak {
override def toString() = "steak"
}
trait Animal {
type Food
def defaultFood(): Food
}
class Bulldog extends Animal {
type Food = Steak
def defaultFood() = new Steak {}
}
class Kennel[A <: Animal](val animal: A)
object Test extends App {
def getSupply[A <: Animal](kennel: Kennel[A]): kennel.animal.Food = kennel.animal.defaultFood()
val bulldog = new Bulldog()
val kennel = new Kennel(bulldog)
val df2: bulldog.Food = getSupply(kennel)
println(df2) // prints "steak"
}
Upvotes: 3