Scala: ¿Qué es un TypeTag y cómo lo uso?


361

Todo lo que sé sobre TypeTags es que de alguna manera reemplazaron Manifiestos. La información en Internet es escasa y no me proporciona un buen sentido del tema.

Por lo tanto, estaría feliz si alguien compartiera un enlace a algunos materiales útiles en TypeTags, incluidos ejemplos y casos de uso populares. Respuestas detalladas y explicaciones también son bienvenidas.


1
El siguiente artículo de la documentación de Scala describe el qué y el por qué de las etiquetas de tipo, así como también cómo usarlas en su código: docs.scala-lang.org/overviews/reflection/…
btiernay

Respuestas:


563

A TypeTagresuelve el problema de que los tipos de Scala se borran en tiempo de ejecución (borrado de tipos). Si queremos hacer

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

recibiremos advertencias:

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

Para resolver este problema, se presentaron Manifiestos a Scala. Pero tienen el problema de no poder representar muchos tipos útiles, como los tipos dependientes de la ruta:

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

Por lo tanto, son reemplazados por TypeTags , que son mucho más simples de usar y están bien integrados en la nueva API de Reflection. Con ellos podemos resolver el problema anterior sobre los tipos dependientes de la ruta con elegancia:

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

También son fáciles de usar para verificar los parámetros de tipo:

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

En este punto, es extremadamente importante comprender el uso =:=(igualdad de tipos) y <:<(relación de subtipo) para verificaciones de igualdad. Nunca use ==o !=, a menos que sepa absolutamente lo que hace:

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

Este último verifica la igualdad estructural, que a menudo no es lo que se debe hacer porque no le importan cosas como los prefijos (como en el ejemplo).

A TypeTagestá completamente generado por el compilador, eso significa que el compilador crea y completa un TypeTagcuando se llama a un método que espera tal TypeTag. Existen tres formas diferentes de etiquetas:

ClassTagsustituye ClassManifestmientras que TypeTages más o menos el reemplazo de Manifest.

El primero permite trabajar completamente con matrices genéricas:

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 proporciona solo la información necesaria para crear tipos en tiempo de ejecución (que se borran):

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]

Como se puede ver arriba, no les importa el borrado de tipos, por lo tanto, si se desea TypeTagusar tipos "completos" :

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

Como se puede ver, el método tpede TypeTagresultados es completo Type, que es lo mismo que obtenemos cuando typeOfse llama. Por supuesto, es posible usar ambos ClassTagy 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]])

La pregunta que queda ahora es ¿cuál es el sentido de WeakTypeTag? En resumen, TypeTagrepresenta un tipo concreto (esto significa que solo permite tipos completamente instanciados) mientras que WeakTypeTagsolo permite cualquier tipo. La mayoría de las veces a uno no le importa qué es qué (qué medios TypeTagdeben usarse), pero, por ejemplo, cuando se usan macros que deberían funcionar con tipos genéricos, se necesitan:

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 se reemplaza WeakTypeTagcon TypeTagun error se arroja:

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

Para obtener una explicación más detallada sobre las diferencias TypeTagy WeakTypeTagver esta pregunta: Macros Scala: "no se puede crear TypeTag a partir de un tipo T que tenga parámetros de tipo sin resolver"

El sitio de documentación oficial de Scala también contiene una guía para Reflection .


19
¡Gracias por tu respuesta! Algunos comentarios: 1) ==para los tipos representa la igualdad estructural, no la igualdad de referencia. =:=tener en cuenta las equivalencias de tipo (incluso las no obvias, como las equivalencias de prefijos que provienen de diferentes espejos), 2) Ambos TypeTagy AbsTypeTagse basan en espejos. La diferencia es que TypeTagsolo permite tipos completamente instanciados (es decir, sin ningún tipo de parámetros o referencias de miembros de tipo abstracto), 3) Una explicación detallada está aquí: stackoverflow.com/questions/12093752
Eugene Burmako

10
4) Los manifiestos tienen el problema de no poder representar muchos tipos útiles. Esencialmente, solo pueden expresar referencias de tipo (tipos simples como Inty tipos genéricos como List[Int]), dejando de lado tales tipos de Scala como, por ejemplo, refinamientos, tipos dependientes de ruta, existenciales, tipos anotados. Además, los manifiestos son un obstáculo, por lo que no pueden usar el vasto conocimiento que posee el compilador para, por ejemplo, calcular la linealización de un tipo, averiguar si un tipo subtipa a otro, etc.
Eugene Burmako

99
5) Las etiquetas de tipo de contraste no están "mejor integradas", simplemente están integradas con la nueva API de reflexión (a diferencia de los manifiestos que no están integrados con nada). Esto proporciona acceso a las etiquetas de tipo a ciertos aspectos del compilador, por ejemplo, a Types.scala(7kloc de código que sabe cómo se admiten los tipos para trabajar juntos), Symbols.scala(3kloc de código que sabe cómo funcionan las tablas de símbolos), etc.
Eugene Burmako

99
6) ClassTages un reemplazo directo exacto para ClassManifest, mientras que TypeTages más o menos un sustituto de Manifest. Más o menos, porque: 1) las etiquetas de tipo no llevan borrados, 2) los manifiestos son un gran truco, y dejamos de emular su comportamiento con etiquetas de tipo. El n. ° 1 se puede solucionar mediante el uso de límites de contexto ClassTag y TypeTag cuando necesita tanto borrados como tipos, y uno generalmente no se preocupa por el n. ° 2, porque es posible deshacerse de todos los hacks y usar la API de reflexión completa en lugar.
Eugene Burmako

11
Realmente espero que el compilador Scala elimine las funciones obsoletas en algún momento, para hacer que el conjunto de funciones disponibles sea más ortogonal. Es por eso que me gusta el nuevo soporte de macros porque proporciona el potencial para limpiar el idioma, separando algunas de las características en bibliotecas independientes que no son parte del idioma base.
Alexandru Nedelcu
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.