drhagen
drhagen

Reputation: 9592

Scala: Correcting type inference of representation type over if statement

This is a follow-up to two questions on representation types, which are type parameters of a trait designed to represent the type underlying a bounded type member (or something like that). I've had success creating instances of classes, e.g ConcreteGarage, that have instances cars of bounded type members CarType.

trait Garage {
  type CarType <: Car[CarType]
  def cars: Seq[CarType]

  def copy(cars: Seq[CarType]): Garage

  def refuel(car: CarType, fuel: CarType#FuelType): Garage = copy(
    cars.map {
      case `car` => car.refuel(fuel)
      case other => other
    })
}

class ConcreteGarage[C <: Car[C]](val cars: Seq[C]) extends Garage {
  type CarType = C
  def copy(cars: Seq[C]) = new ConcreteGarage(cars)
}

trait Car[C <: Car[C]] {
  type FuelType <: Fuel
  def fuel: FuelType

  def copy(fuel: C#FuelType): C

  def refuel(fuel: C#FuelType): C = copy(fuel)
}

class Ferrari(val fuel: Benzin) extends Car[Ferrari] {
  type FuelType = Benzin
  def copy(fuel: Benzin) = new Ferrari(fuel)
}

class Mustang(val fuel: Benzin) extends Car[Mustang] {
  type FuelType = Benzin
  def copy(fuel: Benzin) = new Mustang(fuel)
}

trait Fuel
case class Benzin() extends Fuel

I can easily create instances of Cars like Ferraris and Mustangs and put them into a ConcreteGarage, as long as it's simple:

val newFerrari = new Ferrari(Benzin())
val newMustang = new Mustang(Benzin())

val ferrariGarage = new ConcreteGarage(Seq(newFerrari))
val mustangGarage = new ConcreteGarage(Seq(newMustang))

However, if I merely return one or the other, based on a flag, and try to put the result into a garage, it fails:

val likesFord = true
val new_car = if (likesFord) newFerrari else newMustang

val switchedGarage = new ConcreteGarage(Seq(new_car)) // Fails here

The switch alone works fine, it is the call to ConcreteGarage constructor that fails with the rather mystical error:

error: inferred type arguments [this.Car[_ >: this.Ferrari with this.Mustang <: this.Car[_ >: this.Ferrari with this.Mustang <: ScalaObject]{def fuel: this.Benzin; type FuelType<: this.Benzin}]{def fuel: this.Benzin; type FuelType<: this.Benzin}] do not conform to class ConcreteGarage's type parameter bounds [C <: this.Car[C]]
val switchedGarage = new ConcreteGarage(Seq(new_car)) // Fails here
                     ^

I have tried putting those magic [C <: Car[C]] representation type parameters everywhere, but without success in finding the magic spot.

Upvotes: 2

Views: 214

Answers (1)

0__
0__

Reputation: 67310

There aren't any useful super types to which Ferrari and Mustang can be aliased. You need to wrap the world inside out with this approach.

One possibility is to add the Garage construction as a method to Car.

Another possibility is to define some 'world' which takes care of producing compatible cars and garages:

trait World {
   type CarType <: Car[CarType]
   def newCar() : CarType
   def newGarage(cars: Seq[CarType]) = new ConcreteGarage[CarType](cars)
}

class FerrariWorld extends World {
   type CarType = Ferrari
   def newCar() = new Ferrari(Benzin())
}

class FordWorld extends World {
   type CarType = Mustang
   def newCar() = new Mustang(Benzin())
}

def play(world: World) {
   val car = world.newCar()
   println(car)
   val gar = world.newGarage(Seq(car))
   println(gar)
}

def test(likesFord: Boolean) {
   val w = if(likesFord) new FordWorld else new FerrariWorld
   play(w)
}

test(true)
test(false)

You can see that this can get quite claustrophobic. So it really depends on your target scenario. Path-dependent types always cause future constraints. Consider this rather simple variant with type parameters instead:

trait Fuel { def liters: Int }
trait Make { def color: String }

case class Benzin(liters: Int = 0) extends Fuel
case class Diesel(liters: Int = 0) extends Fuel
case class Ferrari(color: String) extends Make
case class Mustang(color: String) extends Make { def race() { println( "Rrrroar" )}}

case class Car[M <: Make, F <: Fuel](make: M, fuel: F) {
   def refuel(f: F): Car[M, F] = copy(make, f)
}

case class Garage[M <: Make](cars: Seq[Car[M,_]] = Seq.empty) {
   def add(c: Car[M,_]) = copy(cars :+ c)
   def remove(c: Car[M,_]) = copy(cars.filterNot(_ == c))
   def refuel[F <: Fuel](c: Car[M,F], f: F) = copy( cars.map {
      case `c` => c.refuel(f)
      case other => other
   })
}    

val g0 = Garage[Mustang]()
val m  = Car(Mustang("black"), Benzin())
val f  = Car(Ferrari("red"), Benzin())
val g1 = g0.add(f)                // forbidden
val g1 = g0.add(m)                // ok
val g2 = g1.refuel(f, Benzin(45)) // forbidden
val g2 = g1.refuel(m, Diesel(45)) // forbidden
val g2 = g1.refuel(m, Benzin(45)) // ok
g2.cars.foreach(_.make.race())    // ok

Conclusion: Don't get side-tracked...

enter image description here

Upvotes: 2

Related Questions