So, let's say I have a class with a contravariant type parameter:
trait Storage[-T] {
def list(path: String): Seq[String]
def getObject[R <: T](path: String): Future[R]
}
The idea of the type parameter is to constrain the implementation to the upper boundary of types that it can return. So, Storage[Any]
can read anything, while Storage[avro.SpecificRecord]
can read avro records, but not other classes:
def storage: Storage[avro.SpecificRecord]
storage.getObject[MyAvroDoc]("foo") // works
storage.getObject[String]("bar") // fails
Now, I have a utility class that can be used to iterate through objects in a given location:
class StorageIterator[+T](
val storage: Storage[_ >: T],
location: String
)(filter: String => Boolean) extends AbstractIterator[Future[T]] {
val it = storage.list(location).filter(filter)
def hasNext = it.hasNext
def next = storage.getObject[T](it.next)
}
This works, but sometimes I need to access the underlying storage
from the iterator downstream, to read another type of object from an aux location:
def storage: Storage[avro.SpecificRecord]
val iter = new StorageIterator[MyAvroDoc]("foo")
iter.storage.getObject[AuxAvroDoc](aux)
This does not work, of course, because storage
type parameter is a wildcard, and there is no proof that it can be used to read AuxAvroDoc
I try to fix it like this:
class StorageIterator2[P, +T <: P](storage: Storage[P])
extends StorageIterator[T](storage)
This works, but now I have to specify two type params when creating it, and that sucks :(
I tried to work around it by adding a method to the Storage
itself:
trait Storage[-T] {
...
def iterate[R <: T](path: String) =
new StorageIterator2[T, R](this, path)
}
But this doesn't compile because it puts T
into an invariant position :(
And if I make P
contravariant, then StorageIterator2[-P, +T <: P]
fails, because it thinks that P occurs in covariant position in type P of value T
.
This last error I don't understand. Why exactly cannot P
be contravariant here? If this position is really covariant (why is it?) then why does it allow me to specify an invariant parameter there?
Finally, does anyone have an idea how I can work around this? Basically, the idea is to be able to
- Do
storage.iterate[MyAvroDoc]
without having to give it the upper boundary again, and - Do
iterator.storage.getObject[AnotherAvroDoc]
without having to cast the storage to prove that it can read this type of object.
Any ideas are appreciated.