Wie komme ich um das Löschen von Schriftarten auf Scala herum? Oder warum kann ich den Typparameter meiner Sammlungen nicht abrufen?

370

Es ist eine traurige Tatsache in Scala, dass Sie, wenn Sie eine Liste [Int] instanziieren, überprüfen können, ob Ihre Instanz eine Liste ist, und Sie können überprüfen, ob jedes einzelne Element davon eine Int ist, aber nicht, dass es eine Liste ist [ Int], wie leicht zu überprüfen ist:

scala> List(1,2,3) match {
     | case l : List[String] => println("A list of strings?!")
     | case _ => println("Ok")
     | }
warning: there were unchecked warnings; re-run with -unchecked for details
A list of strings?!

Mit der Option -unchecked liegt die Schuld direkt beim Löschen des Typs:

scala>  List(1,2,3) match {
     |  case l : List[String] => println("A list of strings?!")
     |  case _ => println("Ok")
     |  }
<console>:6: warning: non variable type-argument String in type pattern is unchecked since it is eliminated by erasure
        case l : List[String] => println("A list of strings?!")
                 ^
A list of strings?!

Warum ist das so und wie komme ich darum herum?

Daniel C. Sobral
quelle
Scala 2.8 Beta 1 RC4 hat gerade einige Änderungen an der Funktionsweise des Löschens vorgenommen. Ich bin mir nicht sicher, ob dies Ihre Frage direkt betrifft.
Scott Morrison
1
Das ist genau das, was Arten Löschung zu , das sich geändert hat. Der Kurzfilm kann als " Vorschlag " zusammengefasst werden : Das Löschen von "Objekt mit A" ist "A" anstelle von "Objekt". Die tatsächliche Spezifikation ist etwas komplexer. Es geht jedenfalls um Mixins, und diese Frage betrifft Generika.
Daniel C. Sobral
Vielen Dank für die Klarstellung - ich bin ein Scala-Neuling. Ich denke, gerade jetzt ist eine schlechte Zeit, um in die Scala zu springen. Früher hätte ich die Änderungen in 2.8 von einer guten Basis lernen können, später musste ich den Unterschied nie kennen!
Scott Morrison
1
Hier ist eine etwas verwandte Frage zu TypeTags .
pvorb
2
Beim Laufen scala 2.10.2sah ich stattdessen diese Warnung: <console>:9: warning: fruitless type test: a value of type List[Int] cannot also be a List[String] (but still might match its erasure) case list: List[String] => println("a list of strings?") ^Ich finde Ihre Frage und Antwort sehr hilfreich, bin mir aber nicht sicher, ob diese aktualisierte Warnung für die Leser nützlich ist.
Kevin Meredith

Antworten:

243

Diese Antwort verwendet die Manifest-API, die ab Scala 2.10 veraltet ist. Weitere aktuelle Lösungen finden Sie in den Antworten unten.

Scala wurde mit Type Erasure definiert, da die Java Virtual Machine (JVM) im Gegensatz zu Java keine Generika erhielt. Dies bedeutet, dass zur Laufzeit nur die Klasse vorhanden ist, nicht ihre Typparameter. In diesem Beispiel weiß JVM, dass es eine verarbeitet scala.collection.immutable.List, aber nicht, dass diese Liste mit parametrisiert ist Int.

Glücklicherweise gibt es in Scala eine Funktion, mit der Sie das umgehen können. Es ist das Manifest . Ein Manifest ist eine Klasse, deren Instanzen Objekte sind, die Typen darstellen. Da es sich bei diesen Instanzen um Objekte handelt, können Sie sie weitergeben, speichern und im Allgemeinen Methoden für sie aufrufen. Mit der Unterstützung impliziter Parameter wird es zu einem sehr leistungsfähigen Werkzeug. Nehmen Sie zum Beispiel das folgende Beispiel:

object Registry {
  import scala.reflect.Manifest

  private var map= Map.empty[Any,(Manifest[_], Any)] 

  def register[T](name: Any, item: T)(implicit m: Manifest[T]) {
    map = map.updated(name, m -> item)
  }

  def get[T](key:Any)(implicit m : Manifest[T]): Option[T] = {
    map get key flatMap {
      case (om, s) => if (om <:< m) Some(s.asInstanceOf[T]) else None
    }     
  }
}

scala> Registry.register("a", List(1,2,3))

scala> Registry.get[List[Int]]("a")
res6: Option[List[Int]] = Some(List(1, 2, 3))

scala> Registry.get[List[String]]("a")
res7: Option[List[String]] = None

Beim Speichern eines Elements speichern wir auch ein "Manifest" davon. Ein Manifest ist eine Klasse, deren Instanzen Scala-Typen darstellen. Diese Objekte enthalten mehr Informationen als JVM, sodass wir den vollständigen, parametrisierten Typ testen können.

Beachten Sie jedoch, dass sich a Manifestimmer noch weiterentwickelt. Als Beispiel für seine Einschränkungen weiß es derzeit nichts über Varianz und geht davon aus, dass alles eine Co-Variante ist. Ich gehe davon aus, dass es stabiler und solider wird, sobald die derzeit in Entwicklung befindliche Scala-Reflexionsbibliothek fertig ist.

Daniel C. Sobral
quelle
3
Die getMethode kann definiert werden als for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup
4
@ Aaron Sehr guter Vorschlag, aber ich befürchte, er könnte den Code für Leute verdecken, die für Scala relativ neu sind. Ich hatte selbst keine große Erfahrung mit Scala, als ich diesen Code schrieb, bevor ich ihn in diese Frage / Antwort einfügte.
Daniel C. Sobral
6
@KimStebel Weißt du, dass diese TypeTagtatsächlich automatisch für den Mustervergleich verwendet werden? Cool, was?
Daniel C. Sobral
1
Cool! Vielleicht sollten Sie das der Antwort hinzufügen.
Kim Stebel
1
Um meine eigene Frage oben zu beantworten: Ja, der Compiler generiert die Manifest Parameter selbst, siehe: stackoverflow.com/a/11495793/694469 "Die Instanz [manifest / type-tag] [...] wird implizit vom Compiler erstellt "
KajMagnus
96

Sie können dies mit TypeTags tun (wie Daniel bereits erwähnt, aber ich werde es nur explizit formulieren):

import scala.reflect.runtime.universe._
def matchList[A: TypeTag](list: List[A]) = list match {
  case strlist: List[String @unchecked] if typeOf[A] =:= typeOf[String] => println("A list of strings!")
  case intlist: List[Int @unchecked] if typeOf[A] =:= typeOf[Int] => println("A list of ints!")
}

Sie können dies auch mit ClassTags tun (wodurch Sie sich nicht auf Scala-Reflect verlassen müssen):

import scala.reflect.{ClassTag, classTag}
def matchList2[A : ClassTag](list: List[A]) = list match {
  case strlist: List[String @unchecked] if classTag[A] == classTag[String] => println("A List of strings!")
  case intlist: List[Int @unchecked] if classTag[A] == classTag[Int] => println("A list of ints!")
}

ClassTags können verwendet werden, solange Sie nicht erwarten, dass der Typparameter Aselbst ein generischer Typ ist.

Leider ist es etwas ausführlich und Sie benötigen die Annotation @unchecked, um eine Compiler-Warnung zu unterdrücken. Das TypeTag wird möglicherweise in Zukunft vom Compiler automatisch in die Musterübereinstimmung integriert: https://issues.scala-lang.org/browse/SI-6517

tksfz
quelle
2
Wie wäre es mit unnötigem Entfernen, [List String @unchecked]da es dieser Musterübereinstimmung nichts hinzufügt (nur die Verwendung reicht case strlist if typeOf[A] =:= typeOf[String] =>aus, oder selbst case _ if typeOf[A] =:= typeOf[String] =>wenn die gebundene Variable im Hauptteil von nicht benötigt wird case).
Nader Ghanbari
1
Ich denke, das würde für das gegebene Beispiel funktionieren, aber ich denke, die meisten realen Verwendungen würden von der Art der Elemente profitieren.
Tksfz
Macht in den obigen Beispielen nicht der ungeprüfte Teil vor dem Wachzustand eine Besetzung? Würden Sie nicht eine Klassenumwandlungsausnahme erhalten, wenn Sie die Übereinstimmungen für das erste Objekt durchgehen, das nicht in eine Zeichenfolge umgewandelt werden kann?
Toby
Hm nein, ich glaube, es gibt keine Besetzung vor dem Anwenden des Schutzes - das ungeprüfte Bit ist eine Art No-Op, bis der Code rechts von =>ausgeführt wird. (Und wenn der Code auf dem rhs ausgeführt wird, geben die Wachen eine statische Garantie für die Art der Elemente. Es kann eine Besetzung geben, aber es ist sicher.)
tksfz
Verursacht diese Lösung einen erheblichen Laufzeitaufwand?
stanislav.chetvertkov
65

Du kannst den ... benutzen Typeable Typklasse von formlos verwenden , um das gewünschte Ergebnis zu erhalten.

Beispiel einer REPL-Sitzung,

scala> import shapeless.syntax.typeable._
import shapeless.syntax.typeable._

scala> val l1 : Any = List(1,2,3)
l1: Any = List(1, 2, 3)

scala> l1.cast[List[String]]
res0: Option[List[String]] = None

scala> l1.cast[List[Int]]
res1: Option[List[Int]] = Some(List(1, 2, 3))

Der castVorgang wird angesichts der Typeableverfügbaren In-Scope- Instanzen so genau wie möglich gelöscht .

Miles Sabin
quelle
14
Es ist zu beachten, dass die "Cast" -Operation die gesamte Sammlung und ihre Untersammlungen rekursiv durchläuft und prüft, ob alle beteiligten Werte vom richtigen Typ sind. ( Dh l1.cast[List[String]]ungefähr for (x<-l1) assert(x.isInstanceOf[String]) Bei großen Datenstrukturen oder wenn die Casts sehr häufig auftreten, kann dies ein inakzeptabler Overhead sein.
Dominique Unruh
16

Ich habe eine relativ einfache Lösung gefunden, die in Situationen mit eingeschränkter Verwendung ausreicht und im Wesentlichen parametrisierte Typen umschließt, die unter dem Typlöschproblem in Wrapper-Klassen leiden, die in einer Übereinstimmungsanweisung verwendet werden können.

case class StringListHolder(list:List[String])

StringListHolder(List("str1","str2")) match {
    case holder: StringListHolder => holder.list foreach println
}

Dies hat die erwartete Ausgabe und begrenzt den Inhalt unserer Fallklasse auf den gewünschten Typ, String Lists.

Weitere Details hier: http://www.scalafied.com/?p=60

dreimal jamie
quelle
14

Es gibt eine Möglichkeit, das Problem der Typlöschung in Scala zu lösen. Unter Überwinden der Typlöschung in Matching 1 und Überwinden der Typlöschung in Matching 2 (Varianz) finden Sie einige Erläuterungen zum Codieren einiger Helfer, um die Typen, einschließlich Varianz, für den Matching zu verpacken.

axaluss
quelle
Dadurch wird das Löschen des Typs nicht überwunden. In seinem Beispiel wird val x ausgeführt: Any = List (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s "No match")} erzeugt "No match"
user48956
Sie könnten sich Scala 2.10-Makros ansehen.
Alex
11

Ich habe eine etwas bessere Problemumgehung für diese Einschränkung der ansonsten fantastischen Sprache gefunden.

In Scala tritt das Problem der Typlöschung bei Arrays nicht auf. Ich denke, es ist einfacher, dies anhand eines Beispiels zu demonstrieren.

Nehmen wir an, wir haben eine Liste von (Int, String), dann gibt das Folgende eine Art Löschwarnung

x match {
  case l:List[(Int, String)] => 
  ...
}

Um dies zu umgehen, erstellen Sie zunächst eine Fallklasse:

case class IntString(i:Int, s:String)

dann mache im Pattern Matching so etwas wie:

x match {
  case a:Array[IntString] => 
  ...
}

das scheint perfekt zu funktionieren.

Dies erfordert geringfügige Änderungen in Ihrem Code, um mit Arrays anstelle von Listen zu arbeiten, sollte jedoch kein großes Problem darstellen.

Beachten Sie, dass bei Verwendung case a:Array[(Int, String)]weiterhin eine Warnung zum Löschen des Typs ausgegeben wird. Daher muss eine neue Containerklasse verwendet werden (in diesem Beispiel IntString).

Jus12
quelle
10
"Einschränkung der ansonsten fantastischen Sprache" ist weniger eine Einschränkung von Scala als vielmehr eine Einschränkung der JVM. Vielleicht hätte Scala so entworfen werden können, dass es Typinformationen enthält, wie sie auf der JVM ausgeführt wurden, aber ich glaube nicht, dass ein solches Design die Interoperabilität mit Java bewahrt hätte (dh, wie entworfen, können Sie Scala von Java aus aufrufen.)
Carl G.
1
Im Anschluss daran ist die Unterstützung von reified Generics für Scala in .NET / CLR eine fortlaufende Möglichkeit.
Carl G
6

Da Java den tatsächlichen Elementtyp nicht kennt, fand ich es am nützlichsten, ihn nur zu verwenden List[_]. Dann verschwindet die Warnung und der Code beschreibt die Realität - es ist eine Liste von etwas Unbekanntem.

rained_in
quelle
4

Ich frage mich, ob dies eine geeignete Problemumgehung ist:

scala> List(1,2,3) match {
     |    case List(_: String, _*) => println("A list of strings?!")
     |    case _ => println("Ok")
     | }

Es stimmt nicht mit dem Fall "leere Liste" überein, aber es gibt einen Kompilierungsfehler, keine Warnung!

error: type mismatch;
found:     String
requirerd: Int

Dies scheint auf der anderen Seite zu funktionieren ....

scala> List(1,2,3) match {
     |    case List(_: Int, _*) => println("A list of ints")
     |    case _ => println("Ok")
     | }

Ist es nicht noch besser oder verpasse ich den Punkt hier?

agilesteel
quelle
3
Funktioniert nicht mit List (1, "a", "b"), die Typ - Liste [Alle]
sullivan-
1
Obwohl Sullivans Argument richtig ist und es verwandte Probleme mit der Vererbung gibt, fand ich dies dennoch nützlich.
Seth
0

Ich wollte eine Antwort hinzufügen, die das Problem verallgemeinert: Wie erhalte ich zur Laufzeit eine String-Darstellung des Typs meiner Liste?

import scala.reflect.runtime.universe._

def whatListAmI[A : TypeTag](list : List[A]) = {
    if (typeTag[A] == typeTag[java.lang.String]) // note that typeTag[String] does not match due to type alias being a different type
        println("its a String")
    else if (typeTag[A] == typeTag[Int])
        println("its a Int")

    s"A List of ${typeTag[A].tpe.toString}"
}

val listInt = List(1,2,3)
val listString = List("a", "b", "c")

println(whatListAmI(listInt))
println(whatListAmI(listString))
Steve Robinson-Burns
quelle
-18

Verwenden des Pattern Match Guard

    list match  {
        case x:List if x.isInstanceOf(List[String]) => do sth
        case x:List if x.isInstanceOf(List[Int]) => do sth else
     }
Huangmao Quan
quelle
4
Der Grund, warum dies nicht funktioniert, ist, dass isInstanceOfeine Laufzeitprüfung basierend auf den Typinformationen durchgeführt wird, die der JVM zur Verfügung stehen. Und diese Laufzeitinformationen enthalten nicht das Argument type toList (aufgrund der Typlöschung).
Dominique Unruh