wake-up-neo.com

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

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

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

354
Sergey Weiss

Ein TypeTag behebt das Problem, dass die Typen von Scala 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 viele nützliche Typen darstellen können, wie z. B. pfadabhängige Typen:

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 = [email protected]
b1: f1.Bar = [email protected]

scala> val f2 = new Foo;val b2 = new f2.Bar
f2: Foo = [email protected]
b2: f2.Bar = [email protected]

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

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

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

Sie werden daher durch TypeTags ersetzt, die viel einfacher zu verwenden und gut in die neue Reflection-API integriert sind. Mit ihnen können wir das obige Problem über wegabhängige Typen auf elegante Weise 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, um =:= (Typ Gleichheit) und <:< (Subtyp-Relation) für Gleichheitsprüfungen. Verwenden Sie niemals == oder !=, sofern Sie nicht genau wissen, 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 auf strukturelle Gleichheit, was oft nicht getan werden sollte, da ihm Dinge wie Präfixe (wie im Beispiel) egal sind.

Ein TypeTag wird vollständig vom Compiler generiert, dh, der Compiler erstellt und füllt ein TypeTag aus, wenn eine Methode aufgerufen wird, die ein solches TypeTag erwartet. Es gibt drei verschiedene Arten von Tags:

ClassTag ersetzt ClassManifest, während TypeTag mehr oder weniger den Ersatz für Manifest darstellt.

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 enthält nur die Informationen, die zum Erstellen von Typen zur Laufzeit erforderlich sind (die vom Typ gelöscht wurden):

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, interessiert es sie nicht, ob ein Typ gelöscht wird. Wenn man also "vollständige" Typen haben möchte, sollte TypeTag 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 man sehen kann, führt die Methode tpe von TypeTag zu einem vollen Type, der derselbe ist, den wir erhalten, wenn typeOf aufgerufen wird. Natürlich ist es möglich, sowohl ClassTag als auch TypeTag zu verwenden:

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 der Sinn von WeakTypeTag ist. Kurz gesagt, TypeTag stellt einen konkreten Typ dar (dies bedeutet, dass nur vollständig instanziierte Typen zulässig sind), während WeakTypeTag nur einen beliebigen Typ zulässt. Die meiste Zeit ist es einem egal, welches das ist (was bedeutet, dass TypeTag verwendet werden soll), aber zum Beispiel, wenn 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 WeakTypeTag durch TypeTag ersetzt, wird ein Fehler ausgegeben:

<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]
                                                      ^

Eine detailliertere Erklärung der Unterschiede zwischen TypeTag und WeakTypeTag finden Sie in dieser Frage: Scala-Makros: "TypeTag kann nicht aus einem Typ T mit ungelösten Typparametern erstellt werden"

Die offizielle Dokumentationsseite von Scala enthält auch eine Anleitung für Reflection .

549
kiritsuku