Come aggirare la cancellazione del tipo su Scala? Oppure, perché non riesco a ottenere il parametro type delle mie raccolte?


370

È un fatto triste della vita su Scala che se si crea un'istanza di un Elenco [Int], è possibile verificare che la propria istanza sia un Elenco e che sia possibile verificare che ogni singolo elemento di esso sia un Int, ma non che sia un Elenco [ Int], come può essere facilmente verificato:

scala> List(1,2,3) match {
     | case l : List[String] => println("A list of strings?!")
     | case _ => println("Ok")
     | }
warning: there were unchecked warnings; re-run with -unchecked for details
A list of strings?!

L'opzione -unchecked mette esattamente la colpa sulla cancellazione del tipo:

scala>  List(1,2,3) match {
     |  case l : List[String] => println("A list of strings?!")
     |  case _ => println("Ok")
     |  }
<console>:6: warning: non variable type-argument String in type pattern is unchecked since it is eliminated by erasure
        case l : List[String] => println("A list of strings?!")
                 ^
A list of strings?!

Perché è così e come posso aggirarlo?


Scala 2.8 Beta 1 RC4 ha appena apportato alcune modifiche al funzionamento della cancellazione del tipo. Non sono sicuro che ciò influisca direttamente sulla tua domanda.
Scott Morrison,

1
Questo è proprio quello che i tipi di cancellazione a , che è cambiato. Il breve può essere riassunto come " Proposta: la cancellazione di" Oggetto con A "è" A "anziché" Oggetto ". " Le specifiche effettive sono piuttosto complesse. Si tratta di mixin, in ogni caso, e questa domanda è preoccupata per i generici.
Daniel C. Sobral,

Grazie per il chiarimento - Sono un nuovo arrivato scala. Sento che in questo momento è un brutto momento per saltare in Scala. In precedenza, avrei potuto imparare i cambiamenti in 2.8 da una buona base, in seguito non avrei mai dovuto sapere la differenza!
Scott Morrison,

1
Ecco una domanda in qualche modo correlata su TypeTags .
pvorb,

2
In esecuzione scala 2.10.2, ho visualizzato invece questo avviso: <console>:9: warning: fruitless type test: a value of type List[Int] cannot also be a List[String] (but still might match its erasure) case list: List[String] => println("a list of strings?") ^trovo la tua domanda e risposta molto utile, ma non sono sicuro che questo avviso aggiornato sia utile per i lettori.
Kevin Meredith,

Risposte:


243

Questa risposta usa Manifest-API, che è obsoleto a partire da Scala 2.10. Si prega di consultare le risposte di seguito per ulteriori soluzioni attuali.

Scala è stata definita con Type Erasure perché la Java Virtual Machine (JVM), a differenza di Java, non ha ottenuto generici. Ciò significa che, in fase di esecuzione, esiste solo la classe, non i suoi parametri di tipo. Nell'esempio, JVM sa che sta gestendo un scala.collection.immutable.List, ma non che questo elenco sia parametrizzato con Int.

Fortunatamente, c'è una funzione in Scala che ti consente di aggirare questo. È il manifest . Un manifest è una classe le cui istanze sono oggetti che rappresentano i tipi. Poiché queste istanze sono oggetti, è possibile passarle in giro, archiviarle e in genere chiamare metodi su di esse. Con il supporto di parametri impliciti, diventa uno strumento molto potente. Prendi il seguente esempio, ad esempio:

object Registry {
  import scala.reflect.Manifest

  private var map= Map.empty[Any,(Manifest[_], Any)] 

  def register[T](name: Any, item: T)(implicit m: Manifest[T]) {
    map = map.updated(name, m -> item)
  }

  def get[T](key:Any)(implicit m : Manifest[T]): Option[T] = {
    map get key flatMap {
      case (om, s) => if (om <:< m) Some(s.asInstanceOf[T]) else None
    }     
  }
}

scala> Registry.register("a", List(1,2,3))

scala> Registry.get[List[Int]]("a")
res6: Option[List[Int]] = Some(List(1, 2, 3))

scala> Registry.get[List[String]]("a")
res7: Option[List[String]] = None

Quando memorizziamo un elemento, memorizziamo anche un "Manifest". Un Manifest è una classe le cui istanze rappresentano i tipi di Scala. Questi oggetti hanno più informazioni di quelle di JVM, che ci consentono di testare il tipo completo e parametrizzato.

Si noti, tuttavia, che a Manifestè ancora una funzione in evoluzione. Come esempio dei suoi limiti, al momento non sa nulla della varianza e presume che tutto sia co-variante. Mi aspetto che diventerà più stabile e solido una volta terminata la biblioteca di riflessione alla Scala, attualmente in fase di sviluppo.


3
Il getmetodo può essere definito come for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup il

4
@Aaron Ottimo suggerimento, ma temo che potrebbe oscurare il codice per le persone relativamente nuove a Scala. Non ho avuto molta esperienza con Scala io stesso quando ho scritto quel codice, che prima o poi l'ho inserito in questa domanda / risposta.
Daniel C. Sobral,

6
@KimStebel Sai che TypeTagvengono effettivamente utilizzati automaticamente sulla corrispondenza dei modelli? Bene, eh?
Daniel C. Sobral,

1
Freddo! Forse dovresti aggiungerlo alla risposta.
Kim Stebel,

1
Per rispondere alla mia domanda appena sopra: Sì, il compilatore genera il Manifestparametro stesso, vedere: stackoverflow.com/a/11495793/694469 "l'istanza [manifest / type-tag] [...] viene creata implicitamente dal compilatore "
KajMagnus

96

Puoi farlo usando TypeTags (come già menzionato Daniel, ma lo spiegherò esplicitamente):

import scala.reflect.runtime.universe._
def matchList[A: TypeTag](list: List[A]) = list match {
  case strlist: List[String @unchecked] if typeOf[A] =:= typeOf[String] => println("A list of strings!")
  case intlist: List[Int @unchecked] if typeOf[A] =:= typeOf[Int] => println("A list of ints!")
}

Puoi anche farlo usando ClassTags (che ti evita di dover dipendere da scala-reflection):

import scala.reflect.{ClassTag, classTag}
def matchList2[A : ClassTag](list: List[A]) = list match {
  case strlist: List[String @unchecked] if classTag[A] == classTag[String] => println("A List of strings!")
  case intlist: List[Int @unchecked] if classTag[A] == classTag[Int] => println("A list of ints!")
}

I ClassTag possono essere utilizzati purché non si preveda che il parametro type sia di Aper sé un tipo generico.

Sfortunatamente è un po 'prolisso e hai bisogno dell'annotazione @unchecked per sopprimere un avviso del compilatore. In futuro il TypeTag può essere incorporato automaticamente nella corrispondenza del modello dal compilatore: https://issues.scala-lang.org/browse/SI-6517


2
Che ne dici di rimuovere inutili [List String @unchecked]in quanto non aggiunge nulla a questa corrispondenza del modello (solo usando case strlist if typeOf[A] =:= typeOf[String] =>lo farà, o anche case _ if typeOf[A] =:= typeOf[String] =>se la variabile associata non è necessaria nel corpo di case).
Nader Ghanbari,

1
Immagino che funzionerebbe per l'esempio dato ma penso che la maggior parte degli usi reali trarrebbe beneficio dall'avere il tipo di elementi.
tksfz,

Negli esempi sopra, la parte non selezionata davanti alla condizione di guardia non fa un cast? Non otterresti un'eccezione di cast di classe quando attraversi le partite sul primo oggetto che non può essere lanciato su una stringa?
Toby,

Hm no, credo che non ci sia cast prima di applicare la guardia - il bit non spuntato è una specie di no-op fino a quando non =>viene eseguito il codice a destra di . (E quando viene eseguito il codice su rhs, le guardie forniscono una garanzia statica sul tipo di elementi. Potrebbe esserci un cast lì, ma è sicuro.)
tksfz

Questa soluzione produce un notevole sovraccarico di runtime?
stanislav.chetvertkov,

65

Puoi utilizzare la Typeableclasse type da informe per ottenere il risultato che stai cercando,

Sessione REPL di esempio,

scala> import shapeless.syntax.typeable._
import shapeless.syntax.typeable._

scala> val l1 : Any = List(1,2,3)
l1: Any = List(1, 2, 3)

scala> l1.cast[List[String]]
res0: Option[List[String]] = None

scala> l1.cast[List[Int]]
res1: Option[List[Int]] = Some(List(1, 2, 3))

L' castoperazione sarà la cancellazione più precisa possibile, date le Typeableistanze di ambito disponibili.


14
Va notato che l'operazione "cast" passerà in modo ricorsivo attraverso l'intera raccolta e le sue raccolte secondarie e verificherà se tutti i valori coinvolti sono del tipo giusto. ( Vale a dire l1.cast[List[String]]approssimativamente for (x<-l1) assert(x.isInstanceOf[String]) Per grandi strutture di dati o se i calchi si verificano molto spesso, questo può essere un sovraccarico inaccettabile.
Dominique Unruh,

16

Ho trovato una soluzione relativamente semplice che sarebbe sufficiente in situazioni di uso limitato, essenzialmente avvolgendo i tipi con parametri che soffrirebbero del problema di cancellazione del tipo nelle classi wrapper che possono essere utilizzate in un'istruzione match.

case class StringListHolder(list:List[String])

StringListHolder(List("str1","str2")) match {
    case holder: StringListHolder => holder.list foreach println
}

Questo ha l'output previsto e limita i contenuti della nostra classe case al tipo desiderato, String Lists.

Maggiori dettagli qui: http://www.scalafied.com/?p=60


14

C'è un modo per superare il problema della cancellazione del tipo in Scala. In Superamento della cancellazione del tipo nella corrispondenza 1 e Superamento della cancellazione del tipo nella corrispondenza 2 (Varianza) sono riportate alcune spiegazioni su come codificare alcuni helper per avvolgere i tipi, inclusa la Varianza, per la corrispondenza.


Questo non supera la cancellazione del tipo. Nel suo esempio, facendo val x: Any = List (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s "Nessuna corrispondenza")} produce "Nessuna corrispondenza"
user48956

puoi dare un'occhiata alle macro di scala 2.10.
Alex,

11

Ho trovato una soluzione leggermente migliore per questa limitazione del linguaggio altrimenti fantastico.

In Scala, il problema della cancellazione del tipo non si verifica con le matrici. Penso che sia più facile dimostrarlo con un esempio.

Supponiamo di avere un elenco di (Int, String), quindi quanto segue fornisce un avviso di cancellazione del tipo

x match {
  case l:List[(Int, String)] => 
  ...
}

Per ovviare a questo, creare prima una classe di casi:

case class IntString(i:Int, s:String)

quindi nella corrispondenza del modello fai qualcosa del tipo:

x match {
  case a:Array[IntString] => 
  ...
}

che sembra funzionare perfettamente.

Ciò richiederà modifiche minori nel codice per funzionare con le matrici anziché con gli elenchi, ma non dovrebbe essere un grosso problema.

Si noti che l'utilizzo case a:Array[(Int, String)]fornirà comunque un avviso di cancellazione del tipo, quindi è necessario utilizzare una nuova classe contenitore (in questo esempio IntString).


10
"limitazione del linguaggio altrimenti fantastico" è meno una limitazione di Scala e più una limitazione della JVM. Forse Scala avrebbe potuto essere progettato per includere informazioni sul tipo durante l'esecuzione su JVM, ma non credo che un progetto del genere avrebbe preservato l'interoperabilità con Java (ovvero, come progettato, è possibile chiamare Scala da Java.)
Carl G

1
Come follow-up, il supporto per generici reificati per Scala in .NET / CLR è una possibilità continua.
Carl G,

6

Poiché Java non conosce il tipo di elemento effettivo, l'ho trovato molto utile da usare List[_]. Quindi l'avvertimento scompare e il codice descrive la realtà: è un elenco di qualcosa di sconosciuto.


4

Mi chiedo se questa è una soluzione alternativa adatta:

scala> List(1,2,3) match {
     |    case List(_: String, _*) => println("A list of strings?!")
     |    case _ => println("Ok")
     | }

Non corrisponde al caso "elenco vuoto", ma genera un errore di compilazione, non un avviso!

error: type mismatch;
found:     String
requirerd: Int

Questo d'altra parte sembra funzionare ...

scala> List(1,2,3) match {
     |    case List(_: Int, _*) => println("A list of ints")
     |    case _ => println("Ok")
     | }

Non è forse ancora meglio o mi sto perdendo il punto qui?


3
Non funziona con List (1, "a", "b"), che ha il tipo List [Any]
sullivan-

1
Sebbene il punto di Sullivan sia corretto e ci siano problemi correlati con l'eredità, l'ho ancora trovato utile.
Seth,


0

Volevo aggiungere una risposta che generalizzi il problema a: Come ottenere una rappresentazione String del tipo del mio elenco in fase di esecuzione

import scala.reflect.runtime.universe._

def whatListAmI[A : TypeTag](list : List[A]) = {
    if (typeTag[A] == typeTag[java.lang.String]) // note that typeTag[String] does not match due to type alias being a different type
        println("its a String")
    else if (typeTag[A] == typeTag[Int])
        println("its a Int")

    s"A List of ${typeTag[A].tpe.toString}"
}

val listInt = List(1,2,3)
val listString = List("a", "b", "c")

println(whatListAmI(listInt))
println(whatListAmI(listString))

-18

Usando la protezione della corrispondenza del modello

    list match  {
        case x:List if x.isInstanceOf(List[String]) => do sth
        case x:List if x.isInstanceOf(List[Int]) => do sth else
     }

4
Il motivo per cui questo non funzionerà è che isInstanceOfesegue un controllo di runtime in base alle informazioni sul tipo disponibili per la JVM. E le informazioni di runtime non conterranno l'argomento type in List(a causa della cancellazione del tipo).
Dominique Unruh,
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.