Comment contourner l'effacement des caractères sur Scala? Ou, pourquoi ne puis-je pas obtenir le paramètre de type de mes collections?

370

C'est une triste réalité de Scala que si vous instanciez une liste [Int], vous pouvez vérifier que votre instance est une liste, et vous pouvez vérifier que tout élément individuel de celle-ci est un int, mais pas qu'il s'agit d'une liste [ Int], comme cela peut être facilement vérifié:

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'option -unchecked met le blâme carrément sur l'effacement de type:

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

Pourquoi cela, et comment puis-je contourner ce problème?

Daniel C. Sobral
la source
Scala 2.8 Beta 1 RC4 vient de modifier le fonctionnement de l'effacement des types. Je ne sais pas si cela affecte directement votre question.
Scott Morrison
1
C'est exactement ce type d' effacement à , qui a changé. Le résumé peut être résumé comme " Proposition: L'effacement de" Objet avec A "est" A "au lieu de" Objet ". " La spécification réelle est plutôt plus complexe. Il s'agit en tout cas de mixins, et cette question concerne les génériques.
Daniel C.Sobral
Merci pour la clarification - je suis un nouveau venu scala. J'ai l'impression que le moment est mal choisi pour sauter à Scala. Plus tôt, j'aurais pu apprendre les changements dans 2.8 à partir d'une bonne base, plus tard je n'aurais jamais à faire la différence!
Scott Morrison
1
Voici une question au sujet quelque peu lié TypeTags .
pvorb
2
En cours d'exécution scala 2.10.2, j'ai vu cet avertissement à la place: <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?") ^je trouve que votre question et votre réponse sont très utiles, mais je ne sais pas si cet avertissement mis à jour est utile aux lecteurs.
Kevin Meredith

Réponses:

243

Cette réponse utilise Manifest-API, qui est obsolète depuis Scala 2.10. Veuillez consulter les réponses ci-dessous pour des solutions plus actuelles.

Scala a été défini avec Type Erasure car la machine virtuelle Java (JVM), contrairement à Java, n'a pas reçu de génériques. Cela signifie qu'au moment de l'exécution, seule la classe existe, pas ses paramètres de type. Dans l'exemple, la JVM sait qu'elle gère un scala.collection.immutable.List, mais pas que cette liste est paramétrée avec Int.

Heureusement, il existe une fonctionnalité dans Scala qui vous permet de contourner cela. C'est le manifeste . Un manifeste est une classe dont les instances sont des objets représentant des types. Étant donné que ces instances sont des objets, vous pouvez les transmettre, les stocker et généralement y appeler des méthodes. Avec la prise en charge de paramètres implicites, il devient un outil très puissant. Prenons l'exemple suivant, par exemple:

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

Lorsque nous stockons un élément, nous en stockons également un "manifeste". Un manifeste est une classe dont les instances représentent des types Scala. Ces objets ont plus d'informations que la JVM, ce qui nous permet de tester le type complet et paramétré.

Notez, cependant, que a Manifestest toujours une fonction évolutive. À titre d'exemple de ses limites, il ne connaît actuellement rien de la variance et suppose que tout est co-variant. Je m'attends à ce qu'il devienne plus stable et solide une fois la bibliothèque de réflexion Scala, actuellement en cours de développement, terminée.

Daniel C. Sobral
la source
3
La getméthode peut être définie comme for ((om, v) <- _map get key if om <:< m) yield v.asInstanceOf[T].
Aaron Novstrup
4
@Aaron Très bonne suggestion, mais je crains qu'elle ne masque le code pour les personnes relativement nouvelles à Scala. Je n'étais pas très expérimenté avec Scala moi-même quand j'ai écrit ce code, ce qui était quelque temps avant de le mettre dans cette question / réponse.
Daniel C.Sobral
6
@KimStebel Vous savez qu'ils TypeTagsont en fait automatiquement utilisés pour la correspondance de motifs? Cool, hein?
Daniel C.Sobral
1
Cool! Vous devriez peut-être ajouter cela à la réponse.
Kim Stebel
1
Pour répondre à ma propre question juste au-dessus: oui, le compilateur génère le Manifestparamètre lui-même, voir: stackoverflow.com/a/11495793/694469 "l'instance [manifest / type-tag] [...] est créée implicitement par le compilateur "
KajMagnus
96

Vous pouvez le faire en utilisant TypeTags (comme Daniel le mentionne déjà, mais je vais simplement l'expliquer explicitement):

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

Vous pouvez également le faire à l'aide de ClassTags (ce qui vous évite d'avoir à dépendre de scala-reflect):

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

Les ClassTags peuvent être utilisés tant que vous ne vous attendez pas à ce que le paramètre type Asoit lui-même un type générique.

Malheureusement, c'est un peu verbeux et vous avez besoin de l'annotation @unchecked pour supprimer un avertissement du compilateur. Le TypeTag peut être incorporé dans la correspondance de modèle automatiquement par le compilateur à l'avenir: https://issues.scala-lang.org/browse/SI-6517

tksfz
la source
2
Qu'en est-il de la suppression inutile [List String @unchecked]car elle n'ajoute rien à cette correspondance de modèle (une simple utilisation le case strlist if typeOf[A] =:= typeOf[String] =>fera, ou même case _ if typeOf[A] =:= typeOf[String] =>si la variable liée n'est pas nécessaire dans le corps de la case).
Nader Ghanbari
1
Je suppose que cela fonctionnerait pour l'exemple donné, mais je pense que la plupart des utilisations réelles gagneraient à avoir le type des éléments.
tksfz
Dans les exemples ci-dessus, la partie non vérifiée devant la condition de garde ne fait-elle pas un plâtre? N'obtiendrez-vous pas une exception de conversion de classe lorsque vous effectuez les correspondances sur le premier objet qui ne peuvent pas être converties en chaîne?
Toby
Hm non, je crois qu'il n'y a pas de distribution avant d'appliquer la garde - le bit non contrôlé est en quelque sorte un no-op jusqu'à ce que le code à droite de =>soit exécuté. (Et lorsque le code sur le rhs est exécuté, les gardes fournissent une garantie statique sur le type des éléments. Il peut y avoir un casting là, mais c'est sûr.)
tksfz
Cette solution génère-t-elle une surcharge d'exécution importante?
stanislav.chetvertkov
65

Vous pouvez utiliser la Typeableclasse de type de shapeless pour obtenir le résultat que vous recherchez,

Exemple de session REPL,

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' castopération sera aussi précise que possible en raison de l'effacement Typeabledisponible dans le champ d'application .

Miles Sabin
la source
14
Il convient de noter que l'opération "cast" parcourra récursivement toute la collection et ses sous-collections et vérifiera si toutes les valeurs impliquées sont du bon type. (C'est-à-dire, à l1.cast[List[String]]peu près for (x<-l1) assert(x.isInstanceOf[String]) Pour les grandes infrastructures de données ou si les transtypages se produisent très souvent, cela peut être une surcharge inacceptable.
Dominique Unruh
16

J'ai trouvé une solution relativement simple qui suffirait dans des situations d'utilisation limitée, enveloppant essentiellement les types paramétrés qui souffriraient du problème d'effacement de type dans les classes wrapper qui peuvent être utilisées dans une instruction de correspondance.

case class StringListHolder(list:List[String])

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

Cela a la sortie attendue et limite le contenu de notre classe de cas au type souhaité, String Lists.

Plus de détails ici: http://www.scalafied.com/?p=60

thricejamie
la source
14

Il existe un moyen de surmonter le problème d'effacement de type dans Scala. Dans Surmonter l'effacement de type dans la correspondance 1 et Surmonter l'effacement de type dans la correspondance 2 (variance) sont des explications sur la façon de coder certains assistants pour encapsuler les types, y compris la variance, pour la correspondance.

axaluss
la source
Cela ne surmonte pas l'effacement de type. Dans son exemple, faire val x: Any = List (1,2,3); x match {case IntList (l) => println (s "Match $ {l (1)}"); case _ => println (s "No match")} produit "No match"
user48956
vous pourriez jeter un œil aux macros scala 2.10.
Alex
11

J'ai trouvé une solution de contournement légèrement meilleure pour cette limitation du langage par ailleurs génial.

Dans Scala, le problème de l'effacement de type ne se produit pas avec les tableaux. Je pense qu'il est plus facile de le démontrer avec un exemple.

Disons que nous avons une liste de (Int, String), puis ce qui suit donne un avertissement d'effacement de type

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

Pour contourner ce problème, créez d'abord une classe de cas:

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

puis dans la correspondance de motifs, faites quelque chose comme:

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

qui semble fonctionner parfaitement.

Cela nécessitera des modifications mineures dans votre code pour fonctionner avec des tableaux au lieu de listes, mais cela ne devrait pas être un problème majeur.

Notez que l'utilisation case a:Array[(Int, String)]donnera toujours un avertissement d'effacement de type, il est donc nécessaire d'utiliser une nouvelle classe de conteneur (dans cet exemple, IntString).

Jus12
la source
10
"limitation du langage autrement génial" c'est moins une limitation de Scala et plus une limitation de la JVM. Peut-être que Scala aurait pu être conçu pour inclure des informations de type lors de son exécution sur la machine virtuelle Java, mais je ne pense pas qu'une telle conception aurait préservé l'interopérabilité avec Java (c'est-à-dire que, tel que conçu, vous pouvez appeler Scala à partir de Java.)
Carl G
1
À titre de suivi, la prise en charge des génériques réifiés pour Scala dans .NET / CLR est une possibilité permanente.
Carl G
6

Étant donné que Java ne connaît pas le type d'élément réel, j'ai trouvé qu'il était très utile de simplement l'utiliser List[_]. Ensuite, l'avertissement disparaît et le code décrit la réalité - c'est une liste de quelque chose d'inconnu.

rained_in
la source
4

Je me demande si c'est une solution de contournement adaptée:

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

Il ne correspond pas au cas de la "liste vide", mais il donne une erreur de compilation, pas un avertissement!

error: type mismatch;
found:     String
requirerd: Int

Cela semble d'autre part fonctionner ....

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

N'est-ce pas encore mieux ou est-ce que je manque le point ici?

agilesteel
la source
3
Ne fonctionne pas avec List (1, "a", "b"), qui a le type List [Any]
sullivan-
1
Bien que l'argument de Sullivan soit correct et qu'il y ait des problèmes liés à l'héritage, j'ai toujours trouvé cela utile.
Seth
0

Je voulais ajouter une réponse qui généralise le problème à: Comment obtenir une représentation String du type de ma liste lors de l'exécution

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))
Steve Robinson-Burns
la source
-18

Utilisation de l'allumette de protection de motif

    list match  {
        case x:List if x.isInstanceOf(List[String]) => do sth
        case x:List if x.isInstanceOf(List[Int]) => do sth else
     }
Huangmao Quan
la source
4
La raison pour laquelle celui-ci ne fonctionnera pas est qu'il isInstanceOfeffectue une vérification de l'exécution en fonction des informations de type disponibles pour la JVM. Et ces informations d'exécution ne contiendront pas l'argument type List(en raison de l'effacement de type).
Dominique Unruh