简体   繁体   中英

Scala: Type parameters and inheritance

I'm seeing something I do not understand. I have a hierarchy of (say) Vehicles, a corresponding hierarchy of VehicalReaders, and a VehicleReader object with apply methods:

abstract class VehicleReader[T <: Vehicle] {
...

object VehicleReader {
  def apply[T <: Vehicle](vehicleId: Int): VehicleReader[T] = apply(vehicleType(vehicleId))

  def apply[T <: Vehicle](vehicleType VehicleType): VehicleReader[T] = vehicleType match {
    case VehicleType.Car => new CarReader().asInstanceOf[VehicleReader[T]] 
    ...

Note that when you have more than one apply method, you must specify the return type. I have no issues when there is no need to specify the return type.

The cast (.asInstanceOf[VehicleReader[T]]) is the reason for the question - without it the result is compile errors like:

type mismatch;
 found   : CarReader
 required: VehicleReader[T]
    case VehicleType.Car => new CarReader()
                                  ^

Related questions:

  • Why cannot the compiler see a CarReader as a VehicleReader[T]?
  • What is the proper type parameter and return type to use in this situation?

I suspect the root cause here is that VehicleReader is invariant on its type parameter, but making it covariant does not change the result.

I feel like this should be rather simple (ie, this is easy to accomplish in Java with wildcards).

The problem has a very simple cause and really doesn't have anything to do with variance. Consider even more simple example:

object Example {
  def gimmeAListOf[T]: List[T] = List[Int](10)
}

This snippet captures the main idea of your code. But it is incorrect:

val list = Example.gimmeAListOf[String]

What will be the type of list ? We asked gimmeAListOf method specifically for List[String] , however, it always returns List[Int](10) . Clearly, this is an error.

So, to put it in words, when the method has a signature like method[T]: Example[T] it really declares: " for any type T you give me I will return an instance of Example[T] ". Such types are sometimes called 'universally quantified', or simply 'universal'.

However, this is not your case: your function returns specific instances of VehicleReader[T] depending on the value of its parameter, eg CarReader (which, I presume, extends VehicleReader[Car] ). Suppose I wrote something like:

class House extends Vehicle

val reader = VehicleReader[House](VehicleType.Car)
val house: House = reader.read()  // Assuming there is a method VehicleReader[T].read(): T

The compiler will happily compile this, but I will get ClassCastException when this code is executed.

There are two possible fixes for this situation available. First, you can use existential (or existentially quantified) type, which can be though as a more powerful version of Java wildcards:

def apply(vehicleType: VehicleType): VehicleReader[_] = ...

Signature for this function basically reads "you give me a VehicleType and I return to you an instance of VehicleReader for some type". You will have an object of type VehicleReader[_] ; you cannot say anything about type of its parameter except that this type exists, that's why such types are called existential.

def apply(vehicleType: VehicleType): VehicleReader[T] forSome {type T} = ...

This is an equivalent definition and it is probably more clear from it why these types have such properties - T type is hidden inside parameter, so you don't know anything about it but that it does exist.

But due to this property of existentials you cannot really obtain any information about real type parameters. You cannot get, say, VehicleReader[Car] out of VehicleReader[_] except via direct cast with asInstanceOf , which is dangerous, unless you store a TypeTag / ClassTag for type parameter in VehicleReader and check it before the cast. This is sometimes (in fact, most of time) unwieldy.

That's where the second option comes to the rescue. There is a clear correspondence between VehicleType and VehicleReader[T] in your code, ie when you have specific instance of VehicleType you definitely know concrete T in VehicleReader[T] signature:

VehicleType.Car -> CarReader (<: VehicleReader[Car])
VehicleType.Truck -> TruckReader (<: VehicleReader[Truck])

and so on.

Because of this it makes sense to add type parameter to VehicleType . In this case your method will look like

def apply[T <: Vehicle](vehicleType: VehicleType[T]): VehicleReader[T] = ...

Now input type and output type are directly connected, and the user of this method will be forced to provide a correct instance of VehicleType[T] for that T he wants. This rules out the runtime error I have mentioned earlier.

You will still need asInstanceOf cast though. To avoid casting completely you will have to move VehicleReader instantiation code (eg yours new CarReader() ) to VehicleType , because the only place where you know real value of VehicleType[T] type parameter is where instances of this type are constructed:

sealed trait VehicleType[T <: Vehicle] {
  def newReader: VehicleReader[T]
}

object VehicleType {
  case object Car extends VehicleType[Car] {
    def newReader = new CarReader
  }
  // ... and so on
}

Then VehicleReader factory method will then look very clean and be completely typesafe:

object VehicleReader {
  def apply[T <: Vehicle](vehicleType: VehicleType[T]) = vehicleType.newReader
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM