2012-08-11 10 views
8

Eine Fallklassen copy() Methode soll eine identische Kopie der Instanz bilden, plus irgendwelche Felder durch Namen ersetzen. Dies scheint fehlzuschlagen, wenn die Fallklasse Typparameter mit Manifesten enthält. Die Kopie verliert alle Kenntnisse über die Arten ihrer Parameter.Scala: Wie man Fallklassenkopie macht, manifestieren Informationen

case class Foo[+A : Manifest](a: A) { 
    // Capture manifest so we can observe it 
    // A demonstration with collect would work equally well 
    def myManifest = implicitly[Manifest[_ <: A]] 
} 

case class Bar[A <: Foo[Any]](foo: A) { 
    // A simple copy of foo 
    def fooCopy = foo.copy() 
} 

val foo = Foo(1) 
val bar = Bar(foo) 

println(bar.foo.myManifest)  // Prints "Int" 
println(bar.fooCopy.myManifest) // Prints "Any" 

Warum Foo.copy verlieren das Manifest zu den Parametern und wie kann ich es machen es behalten?

Antwort

15

Mehrere Scala-Eigenheiten interagieren, um dieses Verhalten zu erzielen. Die erste Sache ist, dass Manifest s nicht nur an die geheime implizite Parameterliste im Konstruktor angehängt werden, sondern auch an die Kopiermethode. Es ist bekannt, dass

case class Foo[+A : Manifest](a: A)

für

case class Foo[+A](a: A)(implicit m: Manifest[A])

nur syntaktischer Zucker ist, aber dies wirkt sich auch auf den Kopierkonstruktor, die wie dieser

def copy[B](a: B = a)(implicit m: Manifest[B]) = Foo[B](a)(m)

aussehen würde All diese implicit m s werden von th erstellt Der Compiler wird über die implizite Parameterliste an die Methode gesendet.

Das wäre in Ordnung, solange man die copy-Methode an einem Ort verwendet, an dem der Compiler Foo s Typparameter kannte. Zum Beispiel wird dies außerhalb der Bar-Klasse arbeiten:

val foo = Foo(1) 
val aCopy = foo.copy() 
println(aCopy.myManifest) // Prints "Int" 

Das funktioniert, weil der Compiler folgert, dass foo ist ein Foo[Int] so weiß er, dass foo.a ist ein Int so kann es copy wie folgt aufrufen:

val aCopy = foo.copy()(manifest[Int]())

(Beachten Sie, dass manifest[T]() ist eine Funktion, die eine offensichtliche Darstellung des Typs ist T, zB Manifest[T] mit einem großen „M“. Nicht gezeigt erzeugt das eine dition des Standardparameters in copy.) Es funktioniert auch innerhalb der Foo Klasse, weil es bereits das Manifest enthält, das beim Erstellen der Klasse übergeben wurde. Es würde wie folgt aussehen:

case class Foo[+A : Manifest](a: A) { 
    def myManifest = implicitly[Manifest[_ <: A]] 

    def localCopy = copy() 
} 

val foo = Foo(1) 
println(foo.localCopy.myManifest) // Prints "Int" 

Im ursprünglichen Beispiel wird jedoch nicht in der Bar Klasse wegen der zweiten Besonderheit: Während die Parameter des Typs von Bar innerhalb der Bar Klasse bekannt sind, die Typparameter die Typparameter sind nicht. Er weiß, dass A in Bar ein Foo oder ein SubFoo oder SubSubFoo ist, aber nicht, wenn es ein Foo[Int] oder ein Foo[String] ist. Dies ist natürlich das bekannte Typ-Löschproblem in Scala, aber es scheint hier ein Problem zu sein, selbst wenn es nicht so aussieht, als würde die Klasse irgendetwas mit dem Typ foo s Typparameter tun. Aber denken Sie daran, dass jedes Mal, wenn copy aufgerufen wird, eine geheime Injektion eines Manifests vorliegt, und diese Manifeste überschreiben diejenigen, die vorher dort waren.Da die Bar Klasse war keine Ahnung hat, der Typ-Parameter von foo ist, schafft es nur ein Manifest von Any und sendet diese zusammen wie folgt aus:

def fooCopy = foo.copy()(manifest[Any])

Wenn man die Kontrolle über die Foo Klasse (zB es ist nicht List) dann eine Abhilfe durch all das Kopieren über in der Foo-Klasse zu tun, indem ein Verfahren hinzufügen, die das richtige Kopieren tun werden, wie localCopy oben und gibt das Ergebnis:

case class Bar[A <: Foo[Any]](foo: A) { 
    //def fooCopy = foo.copy() 
    def fooCopy = foo.localCopy 
} 

val bar = Bar(Foo(1)) 
println(bar.fooCopy.myManifest) // Prints "Int" 

Eine andere Lösung ist Foo s Typparameter als manifestierten Typ-Parameter von Bar hinzuzufügen:

case class Bar[A <: Foo[B], B : Manifest](foo: A) { 
    def fooCopy = foo.copy() 
} 

Aber diese skaliert schlecht, wenn Klassenhierarchie groß ist, (d mehr Mitglieder haben Typparameter und diese Klassen haben auch Typparameter, da jede Klasse die Typparameter jeder darunter liegenden Klasse haben muss. Es scheint auch die Typinferenz zu machen ausflippen, wenn ein Bar zu konstruieren versucht:

val bar = Bar(Foo(1)) // Does not compile 

val bar = Bar[Foo[Int], Int](Foo(1)) // Compiles 
+0

Gute Arbeit, mein Freund! –

1

Es gibt zwei Probleme, wie Sie identifiziert. Das erste Problem ist das Typenlöschproblem innerhalb von Bar, wobei Bar nicht den Typ des Manifests Foo kennt. Ich würde persönlich die localCopy Workaround verwenden, die Sie vorgeschlagen haben.

Das zweite Problem ist, dass eine andere implizite wird insgeheim in copy injiziert. Dieses Problem wird behoben, indem der Wert erneut explizit in copy übergeben wird. Zum Beispiel:

scala> case class Foo[+A](a: A)(implicit val m: Manifest[A @uncheckedVariance]) 
defined class Foo 

scala> case class Bar[A <: Foo[Any]](foo: A) { 
    | def fooCopy = foo.copy()(foo.m) 
    | } 
defined class Bar 

scala> val foo = Foo(1) 
foo: Foo[Int] = Foo(1) 

scala> val bar = Bar(foo) 
bar: Bar[Foo[Int]] = Bar(Foo(1)) 

scala> bar.fooCopy.m 
res2: Manifest[Any] = Int 

Wir sehen die Kopie des Int Manifest gehalten hat, aber die Art der fooCopy und res2 ist Manifest[Any] wegen Löschung.

Da ich Zugriff auf die impliziten Beweise benötigt, um die copy zu tun, musste ich die explizite Syntax implicit (hah) anstelle der kontextgebundenen Syntax verwenden. Aber die Verwendung der expliziten Syntax verursacht Fehler:

scala> case class Foo[+A](a: A)(implicit val m: Manifest[A]) 
<console>:7: error: covariant type A occurs in invariant position in type => Manifest[A] of value m 
     case class Foo[+A](a: A)(implicit val m: Manifest[A]) 
             ^
scala> case class Foo[+A](a: A)(implicit val m: Manifest[_ <: A]) 
defined class Foo 

scala> val foo = Foo(1) 
<console>:9: error: No Manifest available for Int. 

WTF? Wie funktioniert die kontextgebundene Syntax und die explizite implicit nicht? Ich grub herum und fand eine Lösung für das Problem: die @uncheckedVariance Annotation.

UPDATE

ich um etwas mehr ausgegraben und fand, dass nur in Scala 2.10 Fallklassen geändert wurden in copy() die Felder aus der ersten Parameterliste kopieren.

Martin sagt: Fall Klasse Ness ist nur auf das erste Argument Liste der Rest sollte nicht kopiert werden verliehen.

Die Details dieser Änderung finden Sie unter https://issues.scala-lang.org/browse/SI-5009.

+0

Ich habe versucht, dies zu tun, wusste aber nicht über '@ uncheckedVariance'. Kann das zu einigen unsauberen Aufgaben führen, oder ist die Notwendigkeit dafür nur ein Artefakt der Tatsache, dass es keine getrennten kovarianten und kontravarianten 'Manifeste' in Scala gibt? – drhagen

+0

Ich denke, es ist nur ein Artefakt. Ich war frustriert, dass die kontextgebundene Syntax funktionierte, aber die explizite implizite Syntax nicht, also grub ich herum und fand '@ uncheckedVariance'. Meine Vermutung ist, dass die kontextgebundene Syntax im Grunde genommen hinter den Kulissen '@ uncheckedVariance' ausführt. – sourcedelica

+0

Informationen zu '@ ​​uncheckedVariance' zur Antwort hinzugefügt. – sourcedelica