Scala: Che cos'è un TypeTag e come si usa?


361

Tutto quello che so su TypeTags è che in qualche modo hanno sostituito Manifests. Le informazioni su Internet sono scarse e non mi danno un buon senso dell'argomento.

Quindi sarei felice se qualcuno condividesse un link ad alcuni materiali utili su TypeTags tra cui esempi e casi d'uso popolari. Sono anche benvenute risposte e spiegazioni dettagliate.


1
Il seguente articolo della documentazione di Scala descrive sia il che cosa il perché dei tag di tipo, sia come usarli nel tuo codice: docs.scala-lang.org/overviews/reflection/…
btiernay

Risposte:


563

A TypeTagrisolve il problema che i tipi di Scala vengono cancellati in fase di esecuzione (tipo di cancellazione). Se vogliamo farlo

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

riceveremo avvisi:

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

Per risolvere questo problema I manifesti furono introdotti in Scala. Ma hanno il problema di non essere in grado di rappresentare molti tipi utili, come i tipi dipendenti dal percorso:

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

Pertanto, vengono sostituiti da TypeTags , che sono entrambi molto più semplici da utilizzare e ben integrati nella nuova API Reflection. Con loro possiamo risolvere elegantemente il problema sopra sui tipi dipendenti dal percorso:

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

Sono anche facili da usare per controllare i parametri del 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

A questo punto, è estremamente importante comprendere l'uso =:=(tipo uguaglianza) e <:<(relazione sottotipo) per i controlli di uguaglianza. Non usare mai ==o !=, a meno che tu non sappia assolutamente cosa fai:

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

Quest'ultimo controlla l'uguaglianza strutturale, che spesso non è ciò che dovrebbe essere fatto perché non si preoccupa di cose come i prefissi (come nell'esempio).

A TypeTagè completamente generato dal compilatore, ciò significa che il compilatore crea e riempie un TypeTagquando si chiama un metodo in attesa di tale TypeTag. Esistono tre diverse forme di tag:

ClassTagsostituisce ClassManifestconsiderando che TypeTagè più o meno il sostituto di Manifest.

Il primo consente di lavorare completamente con array generici:

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 fornisce solo le informazioni necessarie per creare tipi in fase di esecuzione (che vengono cancellati dal tipo):

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]

Come si può vedere sopra, non si preoccupano della cancellazione dei tipi, quindi se si vogliono TypeTagusare tipi "completi" :

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

Come si può vedere, il metodo tpedi TypeTagrisultati è completo Type, che è lo stesso che otteniamo quando typeOfviene chiamato. Naturalmente, è possibile utilizzare entrambi ClassTage 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 domanda rimanente ora è: che senso ha WeakTypeTag? In breve, TypeTagrappresenta un tipo concreto (ciò significa che consente solo tipi completamente istanziati) mentre WeakTypeTagconsente solo qualsiasi tipo. Il più delle volte non ci si preoccupa di quale sia (che significa che TypeTagdovrebbe essere usato), ma per esempio, quando vengono utilizzate macro che dovrebbero funzionare con tipi generici sono necessarie:

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

Se uno sostituisce WeakTypeTagcon TypeTagun errore viene generato:

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

Per una spiegazione più dettagliata delle differenze tra TypeTage WeakTypeTagvedere questa domanda: Macro Scala: "impossibile creare TypeTag da un tipo T con parametri di tipo non risolti"

Il sito di documentazione ufficiale di Scala contiene anche una guida per Reflection .


19
Grazie per la tua risposta! Alcuni commenti: 1) ==per i tipi rappresenta l'uguaglianza strutturale, non l'uguaglianza di riferimento. =:=prendere in considerazione le equivalenze dei tipi (anche quelle non ovvie come le equivalenze dei prefissi che provengono da diversi mirror), 2) Entrambi TypeTage AbsTypeTagsono basati su mirror. La differenza è che TypeTagtipi consente solo completamente esemplificati (ovvero senza alcun parametro di tipo o riferimenti membri tipo astratti), 3) Una spiegazione dettagliata è qui: stackoverflow.com/questions/12093752
Eugene Burmako

10
4) I manifesti hanno il problema di non essere in grado di rappresentare molti tipi utili. In sostanza, possono solo esprimere riferimenti a tipi (tipi semplici come Inte tipi generici come List[Int]), tralasciando tipi Scala come ad esempio perfezionamenti, tipi dipendenti dal percorso, esistenziali, tipi annotati. Anche i manifest sono un fulmine, quindi non possono usare la vasta conoscenza che il compilatore si pone per, diciamo, calcolare una linearizzazione di un tipo, scoprire se un tipo sottotipi un altro, ecc.
Eugene Burmako

9
5) I tag di tipo contrasto non sono "meglio integrati", ma semplicemente integrati con la nuova API di riflessione (diversamente dai manifesti che non sono integrati con nulla). Ciò fornisce l'accesso ai tag di tipo ad alcuni aspetti del compilatore, ad esempio a Types.scala(7kloc di codice che sa come i tipi sono supportati per lavorare insieme), Symbols.scala(3kloc di codice che sa come funzionano le tabelle dei simboli), ecc.
Eugene Burmako,

9
6) ClassTagsostituisce esattamente il drop-in ClassManifest, mentre TypeTagè più o meno un sostituto Manifest. Più o meno, perché: 1) i tag di tipo non portano cancellature, 2) i manifest sono un grande hack e abbiamo rinunciato ad emulare il suo comportamento con i tag di tipo. Il numero 1 può essere risolto utilizzando sia i limiti di contesto ClassTag che TypeTag quando sono necessari sia cancellazioni che tipi, e di solito non ci si preoccupa del # 2, perché diventa possibile eliminare tutti gli hack e utilizzare l'API di riflessione a tutti gli effetti anziché.
Eugene Burmako,

11
Spero davvero che il compilatore Scala si sbarazzerà delle funzionalità deprecate ad un certo punto, per rendere l'insieme delle funzionalità disponibili più ortogonali. Questo è il motivo per cui mi piace il supporto delle nuove macro perché fornisce il potenziale per ripulire la lingua, separando alcune delle funzionalità di librerie indipendenti che non fanno parte della lingua di base.
Alexandru Nedelcu,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.