2017-08-17 2 views
3

So lassen Sie uns sagen, dass ich eine Klasse mit einer kontraTypParameter haben:Ärger mit Typ Varianz

trait Storage[-T] { 
    def list(path: String): Seq[String] 
    def getObject[R <: T](path: String): Future[R] 
    } 

Die Idee vom Typ Parameter ist die Umsetzung an der oberen Grenze der Arten zu beschränken, die sie zurückkehren können. So kann Storage[Any] alles lesen, während Storage[avro.SpecificRecord] avro Aufzeichnungen lesen, aber nicht andere Klassen:

def storage: Storage[avro.SpecificRecord] 
storage.getObject[MyAvroDoc]("foo") // works 
storage.getObject[String]("bar") // fails 

Jetzt habe ich eine Utility-Klasse, die verwendet werden kann, durch Objekte in einem bestimmten Ort zu iterieren:

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) 
} 

Dies funktioniert, aber manchmal muß ich die zugrunde liegenden storage vom Iterator zugreifen, stromabwärts eine andere Art von Objekt aus einer aux Lage zu lesen:

def storage: Storage[avro.SpecificRecord] 
val iter = new StorageIterator[MyAvroDoc]("foo") 
iter.storage.getObject[AuxAvroDoc](aux) 

Dies funktioniert natürlich nicht, weil storage Typ-Parameter ein Platzhalter ist, und es gibt keinen Beweis dafür, dass es verwendet werden kann AuxAvroDoc

Ich versuche zu reparieren es so zu lesen:

class StorageIterator2[P, +T <: P](storage: Storage[P]) 
    extends StorageIterator[T](storage) 

Dies funktioniert, aber jetzt habe ich zwei Typen params angeben, wenn es zu schaffen, und das saugt :( ich habe versucht, um ihn zu arbeiten, indem ein Verfahren zum Storage Zugabe von selbst:

trait Storage[-T] { 
    ... 
    def iterate[R <: T](path: String) = 
    new StorageIterator2[T, R](this, path) 
} 

Aber das kompiliert nicht, weil es setzt T in eine invariante Position :( Und wenn ich P contravariant mache, dann StorageIterator2[-P, +T <: P] schlägt fehl, weil es denkt, dass P occurs in covariant position in type P of value T.

Dieser letzte Fehler verstehe ich nicht. Warum genau kann P hier nicht kontravariant sein? Wenn diese Position wirklich kovariant ist (warum ist das so?), Warum erlaubt es mir, dort einen invarianten Parameter anzugeben?

Hat schließlich jemand eine Idee, wie ich das umgehen kann? Grundsätzlich ist die Idee zu

  1. storage.iterate[MyAvroDoc] Sie der Lage sein, ohne dass die obere Grenze wieder zu ergeben, und
  2. iterator.storage.getObject[AnotherAvroDoc] tun, ohne den Speicher zu werfen, die zu beweisen, dass es diese Art von Objekt lesen .

Alle Ideen sind willkommen.

Antwort

1

StorageIterator2[-P, +T <: P] schlägt fehl, weil es unsinnig ist. Wenn Sie eine StorageIterator2[Foo, Bar] und Bar <: Foo haben, dann ist es auch eine StorageIterator[Nothing, Bar], weil es kontravariant im ersten Parameter ist, aber Nothing hat keine Subtypen, so ist es logisch unmöglich, dass Bar <: Nothing, aber das ist, was eine StorageIterator2 haben muss . Daher kann StorageIterator2 nicht existieren.

Das Grundproblem ist, dass Storage nicht kontravariant sein sollte.Denken Sie daran, was ein Storage[T] ist, in Bezug auf den Vertrag, den es seinen Benutzern gibt. A Storage[T] ist ein Objekt, dem Sie Pfade zuweisen und T s ausgeben. Es macht durchaus Sinn für Storage kovariant zu sein: etwas, das zum Beispiel String s ausgeben kann, gibt auch Any s aus, so macht es logisch Sinn, dass Storage[String] <: Storage[Any]. Sie sagen, dass es umgekehrt sein sollte, dass ein Storage[T] wissen sollte, wie man irgendeinen Subtyp von T ausgibt, aber wie würde das funktionieren? Was passiert, wenn jemand nachträglich einen Subtyp zu T hinzufügt? T kann sogar final sein und immer noch dieses Problem wegen Singleton-Typen haben. Das ist unnötig kompliziert und spiegelt sich in Ihrem Problem wider. Das heißt, sollte Storage

sein
trait Storage[+T] { 
    def list(path: String]: Seq[String] 
    def get(path: String): T 
} 

Dieses dich nicht öffnen zum Beispiel Fehler, den Sie in Ihrer Frage gab:

val x: Storage[avro.SpecificRecord] = ??? 
x.get(???): avro.SpecificRecord // ok 
x.get(???): String // nope 
(x: Storage[String]).get(???) // nope 

nun Ihr Problem ist, dass Sie nicht so etwas wie storage.getObject[T] tun können und die Besetzung muss implizit sein. Sie können stattdessen eine match:

storage.getObject(path) match { 
    case value: CorrectType => ... 
    case _ => // Oops, something else. Error? 
} 

eine einfache asInstanceOf (unerwünscht), oder Sie können, bevor sie eine Hilfsmethode Storage, wie die, die Sie hatten hinzufügen:

def getCast[U <: T](path: String)(implicit tag: ClassTag[U]): Option[U] = tag.unapply(get(path)) 
+0

„A Storage [T ] ist ein Objekt, dem Sie Wege geben und Ts ausgeben. " Das ist nicht wahr. 'T' ist NICHT der Typ des Objekts, das' Storage' zurückgibt, sondern ein allgemeiner Supertyp aller _Objekte, die es zurückgeben kann (gewährt, sie sind alle Ts in gewisser Weise, aber das ist eine technische Eigenschaft, keine begriffliche Eigenschaft). Es muss kontravariant sein, da 'Storage [Any]' wie 'Stroage [Foo]' verwendet werden kann. – Dima

+0

"Sie sagen, dass es andersherum sein sollte, dass ein Storage [T] wissen sollte, wie man einen Subtyp von T ausgibt, aber wie würde das funktionieren?" Ich zeigte ein Beispiel davon in der Frage. Wenn zum Beispiel 'T' eine Basisklasse für avro-Strukturen ist, kann' Storage' jedes Avro-Dokument lesen. Es könnte ein "ThriftStruct" sein, um Sparsamkeit zu lesen, oder "Message" für Protobuf, ein "Produkt" für CSV, usw. Es macht keinen Sinn, für jedes Avro-Dokument in der Anwendung eine separate "Storage" -Klasse zu haben was müsste passieren, wenn es kovariant wäre. – Dima

+0

Casting und 'match''ing könnten ein Workaround sein, aber das ist so ein Java-Weg, Dinge zu tun. Diese Art von Hacks zu vermeiden, ist genau der Zweck einer Typenvarianz. Die Idee ist, zur Kompilierzeit deklarieren zu können: "Dieses Objekt kann jedes avro doc und nichts anderes lesen", um Fehler zur Laufzeit nicht zu erzeugen, wenn es auf etwas trifft, zu dem es nicht soll. – Dima