tribbloid
tribbloid

Reputation: 3868

How to instruct Scala not to upcast abstract type?

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

Answers (2)

Yawar
Yawar

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

slouc
slouc

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

Related Questions