Scala: Was ist ein TypeTag und wie verwende ich ihn?


361

Alles, was ich über TypeTags weiß, ist, dass sie Manifeste irgendwie ersetzt haben. Informationen im Internet sind rar und vermitteln mir keinen guten Einblick in das Thema.

Ich würde mich also freuen, wenn jemand einen Link zu nützlichen Materialien auf TypeTags mit Beispielen und gängigen Anwendungsfällen teilen würde. Detaillierte Antworten und Erklärungen sind ebenfalls willkommen.


1
Der folgende Artikel aus der Scala-Dokumentation beschreibt sowohl das Was als auch das Warum von Typ-Tags sowie deren Verwendung in Ihrem Code: docs.scala-lang.org/overviews/reflection/…
btiernay

Antworten:


563

A TypeTaglöst das Problem, dass Scalas Typen zur Laufzeit gelöscht werden (Typlöschung). Wenn wir wollen

class Foo
class Bar extends Foo

def meth[A](xs: List[A]) = xs match {
  case _: List[String] => "list of strings"
  case _: List[Foo] => "list of foos"
}

Wir werden Warnungen bekommen:

<console>:23: warning: non-variable type argument String in type pattern List[String]↩
is unchecked since it is eliminated by erasure
         case _: List[String] => "list of strings"
                 ^
<console>:24: warning: non-variable type argument Foo in type pattern List[Foo]↩
is unchecked since it is eliminated by erasure
         case _: List[Foo] => "list of foos"
                 ^

Um dieses Problem zu lösen, wurden Manifeste in Scala eingeführt. Sie haben jedoch das Problem, dass sie nicht in der Lage sind, viele nützliche Typen wie pfadabhängige Typen darzustellen:

scala> class Foo{class Bar}
defined class Foo

scala> def m(f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar]) = ev
warning: there were 2 deprecation warnings; re-run with -deprecation for details
m: (f: Foo)(b: f.Bar)(implicit ev: Manifest[f.Bar])Manifest[f.Bar]

scala> val f1 = new Foo;val b1 = new f1.Bar
f1: Foo = Foo@681e731c
b1: f1.Bar = Foo$Bar@271768ab

scala> val f2 = new Foo;val b2 = new f2.Bar
f2: Foo = Foo@3e50039c
b2: f2.Bar = Foo$Bar@771d16b9

scala> val ev1 = m(f1)(b1)
warning: there were 2 deprecation warnings; re-run with -deprecation for details
ev1: Manifest[f1.Bar] = Foo@681e731c.type#Foo$Bar

scala> val ev2 = m(f2)(b2)
warning: there were 2 deprecation warnings; re-run with -deprecation for details
ev2: Manifest[f2.Bar] = Foo@3e50039c.type#Foo$Bar

scala> ev1 == ev2 // they should be different, thus the result is wrong
res28: Boolean = true

Daher werden sie durch TypeTags ersetzt , die sowohl viel einfacher zu verwenden als auch gut in die neue Reflection-API integriert sind. Mit ihnen können wir das obige Problem über pfadabhängige Typen elegant lösen:

scala> def m(f: Foo)(b: f.Bar)(implicit ev: TypeTag[f.Bar]) = ev
m: (f: Foo)(b: f.Bar)(implicit ev: reflect.runtime.universe.TypeTag[f.Bar])↩
reflect.runtime.universe.TypeTag[f.Bar]

scala> val ev1 = m(f1)(b1)
ev1: reflect.runtime.universe.TypeTag[f1.Bar] = TypeTag[f1.Bar]

scala> val ev2 = m(f2)(b2)
ev2: reflect.runtime.universe.TypeTag[f2.Bar] = TypeTag[f2.Bar]

scala> ev1 == ev2 // the result is correct, the type tags are different
res30: Boolean = false

scala> ev1.tpe =:= ev2.tpe // this result is correct, too
res31: Boolean = false

Sie sind auch einfach zu verwenden, um Typparameter zu überprüfen:

import scala.reflect.runtime.universe._

def meth[A : TypeTag](xs: List[A]) = typeOf[A] match {
  case t if t =:= typeOf[String] => "list of strings"
  case t if t <:< typeOf[Foo] => "list of foos"
}

scala> meth(List("string"))
res67: String = list of strings

scala> meth(List(new Bar))
res68: String = list of foos

An dieser Stelle ist es äußerst wichtig zu verstehen, wie =:=(Typgleichheit) und <:<(Subtypbeziehung) für Gleichheitsprüfungen verwendet werden. Verwenden Sie niemals ==oder !=, es sei denn, Sie wissen genau, was Sie tun:

scala> typeOf[List[java.lang.String]] =:= typeOf[List[Predef.String]]
res71: Boolean = true

scala> typeOf[List[java.lang.String]] == typeOf[List[Predef.String]]
res72: Boolean = false

Letzterer prüft die strukturelle Gleichheit, was häufig nicht der Fall ist, da Dinge wie Präfixe (wie im Beispiel) nicht berücksichtigt werden.

A TypeTagist vollständig vom Compiler generiert, dh der Compiler erstellt und füllt a aus, TypeTagwenn man eine Methode aufruft, die eine solche erwartet TypeTag. Es gibt drei verschiedene Arten von Tags:

ClassTagersetzt, ClassManifestwährend TypeTagmehr oder weniger der Ersatz für ist Manifest.

Ersteres ermöglicht die vollständige Arbeit mit generischen Arrays:

scala> import scala.reflect._
import scala.reflect._

scala> def createArr[A](seq: A*) = Array[A](seq: _*)
<console>:22: error: No ClassTag available for A
       def createArr[A](seq: A*) = Array[A](seq: _*)
                                           ^

scala> def createArr[A : ClassTag](seq: A*) = Array[A](seq: _*)
createArr: [A](seq: A*)(implicit evidence$1: scala.reflect.ClassTag[A])Array[A]

scala> createArr(1,2,3)
res78: Array[Int] = Array(1, 2, 3)

scala> createArr("a","b","c")
res79: Array[String] = Array(a, b, c)

ClassTag stellt nur die Informationen bereit, die zum Erstellen von Typen zur Laufzeit erforderlich sind (die vom Typ gelöscht werden):

scala> classTag[Int]
res99: scala.reflect.ClassTag[Int] = ClassTag[int]

scala> classTag[Int].runtimeClass
res100: Class[_] = int

scala> classTag[Int].newArray(3)
res101: Array[Int] = Array(0, 0, 0)

scala> classTag[List[Int]]
res104: scala.reflect.ClassTag[List[Int]] =ClassTag[class scala.collection.immutable.List]

Wie man oben sehen kann, kümmern sie sich nicht um das Löschen von Typen. Wenn man also "vollständige" Typen möchte, TypeTagsollte Folgendes verwendet werden:

scala> typeTag[List[Int]]
res105: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]]

scala> typeTag[List[Int]].tpe
res107: reflect.runtime.universe.Type = scala.List[Int]

scala> typeOf[List[Int]]
res108: reflect.runtime.universe.Type = scala.List[Int]

scala> res107 =:= res108
res109: Boolean = true

Wie kann man das Verfahren sieht tpevon TypeTagErgebnissen in einem vollständigen Type, die gleich ich ist erhalten , wenn typeOfaufgerufen wird. Natürlich ist es möglich, beide zu verwenden, ClassTagund TypeTag:

scala> def m[A : ClassTag : TypeTag] = (classTag[A], typeTag[A])
m: [A](implicit evidence$1: scala.reflect.ClassTag[A],implicit evidence$2: reflect.runtime.universe.TypeTag[A])(scala.reflect.ClassTag[A], reflect.runtime.universe.TypeTag[A])

scala> m[List[Int]]
res36: (scala.reflect.ClassTag[List[Int]],↩
        reflect.runtime.universe.TypeTag[List[Int]]) =(scala.collection.immutable.List,TypeTag[scala.List[Int]])

Die verbleibende Frage ist nun, was ist der Sinn von WeakTypeTag? Kurz gesagt, TypeTagstellt einen konkreten Typ dar (dies bedeutet, dass nur vollständig instanziierte Typen zulässig sind), während WeakTypeTagnur ein beliebiger Typ zulässig ist . Meistens ist es egal, welches was ist (was bedeutet TypeTag, verwendet werden sollte), aber wenn beispielsweise Makros verwendet werden, die mit generischen Typen funktionieren sollen, werden sie benötigt:

object Macro {
  import language.experimental.macros
  import scala.reflect.macros.Context

  def anymacro[A](expr: A): String = macro __anymacro[A]

  def __anymacro[A : c.WeakTypeTag](c: Context)(expr: c.Expr[A]): c.Expr[A] = {
    // to get a Type for A the c.WeakTypeTag context bound must be added
    val aType = implicitly[c.WeakTypeTag[A]].tpe
    ???
  }
}

Wenn man WeakTypeTagdurch TypeTageinen Fehler ersetzt wird geworfen:

<console>:17: error: macro implementation has wrong shape:
 required: (c: scala.reflect.macros.Context)(expr: c.Expr[A]): c.Expr[String]
 found   : (c: scala.reflect.macros.Context)(expr: c.Expr[A])(implicit evidence$1: c.TypeTag[A]): c.Expr[A]
macro implementations cannot have implicit parameters other than WeakTypeTag evidences
             def anymacro[A](expr: A): String = macro __anymacro[A]
                                                      ^

Für eine detailliertere Erklärung der Unterschiede zwischen TypeTagund WeakTypeTagsiehe diese Frage: Scala-Makros: "TypeTag kann nicht aus einem Typ T mit ungelösten Typparametern erstellt werden."

Die offizielle Dokumentationsseite von Scala enthält auch einen Leitfaden für Reflection .


19
Danke für deine Antwort! Einige Kommentare: 1) ==für Typen steht für strukturelle Gleichheit, nicht für Referenzgleichheit. =:=Berücksichtigen Sie Typäquivalenzen (auch nicht offensichtliche wie Äquivalenzen von Präfixen, die von verschiedenen Spiegeln stammen). 2) Beide TypeTagund AbsTypeTagbasieren auf Spiegeln. Der Unterschied besteht darin, dass TypeTagnur vollständig instanziierte Typen zulässig sind (dh ohne Typparameter oder Verweise auf abstrakte Typelemente
Eugene Burmako

10
4) Manifeste haben das Problem, nicht viele nützliche Typen darstellen zu können. Im Wesentlichen können sie nur Typreferenzen (einfache Typen wie Intund generische Typen wie List[Int]) ausdrücken , wobei Scala-Typen wie z. B. Verfeinerungen, pfadabhängige Typen, Existentiale und kommentierte Typen weggelassen werden. Manifeste sind ebenfalls ein Bolt-On, sodass sie das umfangreiche Wissen, über das der Compiler verfügt, nicht nutzen können, um beispielsweise eine Linearisierung eines Typs zu berechnen, herauszufinden, ob ein Typ einen anderen Subtyp
typisiert

9
5) Damit die Tags vom Kontrasttyp nicht "besser integriert" sind, werden sie einfach in die neue Reflexions-API integriert (im Gegensatz zu Manifesten, die in nichts integriert sind). Dies ermöglicht Typ-Tags den Zugriff auf bestimmte Aspekte des Compilers, z. B. auf Types.scala(7 Kloc Code, der weiß, wie Typen für die Zusammenarbeit unterstützt werden), Symbols.scala(3 Kloc Code, der weiß, wie Symboltabellen funktionieren) usw.
Eugene Burmako

9
6) ClassTagist ein exakter Drop-In-Ersatz für ClassManifest, während TypeTages mehr oder weniger ein Ersatz für ist Manifest. Mehr oder weniger, weil: 1) Typ-Tags keine Löschungen enthalten, 2) Manifeste ein großer Hack sind und wir es aufgegeben haben, ihr Verhalten mit Typ-Tags zu emulieren. # 1 kann durch Verwendung von ClassTag- und TypeTag-Kontextgrenzen behoben werden, wenn Sie sowohl Löschungen als auch Typen benötigen, und # 2 ist normalerweise egal, da es möglich wird, alle Hacks wegzuwerfen und die vollwertige Reflection-API zu verwenden stattdessen.
Eugene Burmako

11
Ich hoffe wirklich, dass der Scala-Compiler irgendwann veraltete Features entfernt, um die verfügbaren Features orthogonaler zu gestalten. Aus diesem Grund gefällt mir die Unterstützung für neue Makros, da sie die Möglichkeit bietet, die Sprache zu bereinigen und einige der Funktionen in unabhängigen Bibliotheken zu trennen, die nicht Teil der Basissprache sind.
Alexandru Nedelcu
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.