Scala: Qu'est-ce qu'un TypeTag et comment l'utiliser?


361

Tout ce que je sais sur TypeTags, c'est qu'ils ont en quelque sorte remplacé les manifestes. Les informations sur Internet sont rares et ne me donnent pas une bonne idée du sujet.

Je serais donc heureux si quelqu'un partageait un lien vers des documents utiles sur TypeTags, y compris des exemples et des cas d'utilisation populaires. Les réponses et explications détaillées sont également les bienvenues.


1
L'article suivant de la documentation Scala décrit à la fois le quoi et le pourquoi des balises de type, ainsi que la façon de les utiliser dans votre code: docs.scala-lang.org/overviews/reflection/…
btiernay

Réponses:


563

A TypeTagrésout le problème de l'effacement des types de Scala au moment de l'exécution (effacement de type). Si nous voulons faire

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

nous recevrons des avertissements:

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

Pour résoudre ce problème, des manifestes ont été introduits dans Scala. Mais ils ont le problème de ne pas pouvoir représenter beaucoup de types utiles, comme les types dépendants du chemin:

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

Ainsi, ils sont remplacés par TypeTags , qui sont à la fois beaucoup plus simples à utiliser et bien intégrés dans la nouvelle API Reflection. Avec eux, nous pouvons résoudre le problème ci-dessus à propos des types dépendants du chemin avec élégance:

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

Ils sont également faciles à utiliser pour vérifier les paramètres de type:

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

À ce stade, il est extrêmement important de comprendre l'utilisation de =:=(égalité de type) et <:<(relation de sous-type) pour les vérifications d'égalité. N'utilisez jamais ==ou !=, sauf si vous savez absolument ce que vous faites:

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

Ce dernier vérifie l'égalité structurelle, ce qui n'est souvent pas ce qui devrait être fait car il ne se soucie pas de choses telles que les préfixes (comme dans l'exemple).

A TypeTagest entièrement généré par le compilateur, ce qui signifie que le compilateur crée et remplit un TypeTagquand on appelle une méthode qui attend un tel TypeTag. Il existe trois formes différentes de balises:

ClassTagsubstituts ClassManifestalors que TypeTagc'est plus ou moins le remplacement de Manifest.

Le premier permet de travailler pleinement avec des tableaux génériques:

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 fournit uniquement les informations nécessaires pour créer des types lors de l'exécution (dont le type est effacé):

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]

Comme on peut le voir ci-dessus, ils ne se soucient pas de l'effacement des types, donc si l'on veut que les types "complets" TypeTagsoient utilisés:

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

Comme on peut le voir, la méthode tpedes TypeTagrésultats dans un full Type, qui est la même que nous obtenons quand typeOfest appelée. Bien sûr, il est possible d'utiliser les deux, ClassTagetTypeTag :

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

La question restante est maintenant quel est le sens de WeakTypeTag? En bref, TypeTagreprésente un type concret (cela signifie qu'il n'autorise que les types entièrement instanciés) alors qu'il WeakTypeTagne permet que n'importe quel type. La plupart du temps, on ne se soucie pas de savoir quoi (quels moyens TypeTagdoivent être utilisés), mais par exemple, lorsque des macros sont utilisées qui devraient fonctionner avec des types génériques, elles sont nécessaires:

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

Si l'on remplace WeakTypeTagpar TypeTagune erreur est levée:

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

Pour une explication plus détaillée des différences entre TypeTag et WeakTypeTagvoir cette question: Macros Scala: "ne peut pas créer TypeTag à partir d'un type T ayant des paramètres de type non résolus"

Le site officiel de documentation de Scala contient également un guide pour Reflection .


19
Merci pour votre réponse! Quelques commentaires: 1) ==pour les types représente l'égalité structurelle, pas l'égalité de référence. =:=prendre en compte les équivalences de type (même celles qui ne sont pas évidentes comme les équivalences de préfixes provenant de différents miroirs), 2) Les deux TypeTaget AbsTypeTagsont basés sur des miroirs. La différence est que TypeTagseuls les types entièrement instanciés sont autorisés (c'est-à-dire sans aucun paramètre de type ou référence à des membres de type abstrait), 3) Une explication détaillée est ici: stackoverflow.com/questions/12093752
Eugene Burmako

10
4) Les manifestes ont le problème de ne pas pouvoir représenter beaucoup de types utiles. Essentiellement, ils ne peuvent exprimer que des références de type (types simples tels que Intet types génériques tels que List[Int]), en laissant de côté les types Scala tels que les raffinements, les types dépendants du chemin, les existentiels, les types annotés. Les manifestes sont également un boulon, ils ne peuvent donc pas utiliser les vastes connaissances que le compilateur possède pour, par exemple, calculer une linéarisation d'un type, savoir si un type en sous
typise

9
5) Pour les balises de type contraste ne sont pas "mieux intégrées", elles sont simplement intégrées à la nouvelle API de réflexion (contrairement aux manifestes qui ne sont intégrés à rien). Cela permet aux balises de type d'accéder à certains aspects du compilateur, par exemple à Types.scala(7kloc de code qui sait comment les types sont pris en charge pour fonctionner ensemble), Symbols.scala(3kloc de code qui sait comment fonctionnent les tables de symboles), etc.
Eugene Burmako

9
6) ClassTagest un remplacement exact pour ClassManifest, alors qu'il TypeTagest plus ou moins un substitut Manifest. Plus ou moins, car: 1) les balises de type ne portent pas d'effacement, 2) les manifestes sont un gros hack, et nous avons renoncé à émuler son comportement avec des balises de type. # 1 peut être corrigé en utilisant à la fois les limites de contexte ClassTag et TypeTag lorsque vous avez besoin à la fois d'effacements et de types, et l'un ne se soucie généralement pas de # 2, car il devient possible de jeter tous les hacks et d'utiliser l'API de réflexion à part entière au lieu.
Eugene Burmako

11
J'espère vraiment que le compilateur Scala se débarrassera des fonctionnalités obsolètes à un moment donné, pour rendre l'ensemble des fonctionnalités disponibles plus orthogonal. C'est pourquoi j'aime le nouveau support des macros car il offre la possibilité de nettoyer la langue, en séparant certaines des fonctionnalités dans des bibliothèques indépendantes qui ne font pas partie de la langue de base.
Alexandru Nedelcu
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.