Supposons que nous ayons une classe générique Container
:
case class Container[+A](value: A)
Nous voulons ensuite mettre en correspondance un Container
avec un Double
et un Container
de Any
:
val double = Container(3.3)
var container: Container[Any] = double
Pour ce faire, nous écririons normalement:
container match {
case c: Container[String] => println(c.value.toUpperCase)
case c: Container[Double] => println(math.sqrt(c.value))
case _ => println("_")
}
Cependant, le compilateur émet deux avertissements, un pour chacun des deux premiers cas. Par exemple, le premier avertissement indique: "l'argument de type non variable String dans le modèle de type Container [String] n'est pas coché car il est éliminé par effacement". En raison de l'effacement, il est impossible pendant l'exécution de faire la distinction entre les différents types de conteneurs et la première capture sera appariée. Par conséquent, un conteneur de type Container[Double]
correspondra au premier cas, qui attrape Container[String]
objets, donc la méthode toUpperCase
sera appelée sur un Double
et un Java.lang.ClassCastException
sera jeté.
Comment faire correspondre un Container
paramétré par un type particulier?
En général, la réponse de rarry est correcte, mais dans votre cas, elle peut être simplifiée, car votre conteneur ne contient qu'une seule valeur d'un type générique, vous pouvez donc faire correspondre directement le type de cette valeur:
container match {
case Container(x: String) => println("string")
case Container(x: Double) => println("double")
case _ => println("w00t")
}
Peut-être que cela aidera
def matchContainer[A: Manifest](c: Container[A]) = c match {
case c: Container[String] if manifest <:< manifest[String] => println(c.value.toUpperCase)
case c: Container[Double] if manifest <:< manifest[Double] => println(math.sqrt(c.value))
case c: Container[_] => println("other")
}
Modifier:
Comme l'a souligné Impredicative, Manifest est déconseillé. Au lieu de cela, vous pouvez effectuer les opérations suivantes:
import reflect.runtime.universe._
def matchContainer[A: TypeTag](c: Container[A]) = c match {
case c: Container[String] if typeOf[A] <:< typeOf[String] => println("string: " + c.value.toUpperCase)
case c: Container[Double] if typeOf[A] <:< typeOf[Double] => println("double" + math.sqrt(c.value))
case c: Container[_] => println("other")
}
Une solution de contournement possible pour cela pourrait être d'utiliser isInstanceOf
et asInstanceOf
.
container match {
case Container(x) if x.isInstanceOf[String] =>
println(x.asInstanceOf[String].toUpperCase)
case Container(x) if x.isInstanceOf[Double] =>
println(math.sqrt(x.asInstanceOf[Double]))
case _ => println("_")
}
Cela fonctionne, mais il n'a pas l'air élégant du tout. Le professeur Martin Odersky, le créateur de Scala, dit que isInstanceOf
et asInstanceOf
doivent être évités.
Comme Rob Norris me l'a fait remarquer, sur le forum du cours " Programmation fonctionnelle en Scala " de Coursera, l'appariement par type est une mauvaise pratique: case foo: Bar => ...
. Scala encourage à tirer parti du typage statique et à éviter de vérifier le type lors de l'exécution. Cela est conforme à la philosophie du monde Haskell/ML. Au lieu de faire correspondre les types , les clauses case
doivent correspondre aux constructeurs .
Pour résoudre le problème de correspondance Container
, un conteneur spécial pour chaque type peut être défini:
class Container[+A](val value: A)
case class StringContainer(override val value: String)
extends Container(value)
case class DoubleContainer(override val value: Double)
extends Container(value)
Et maintenant les constructeurs seront mis en correspondance, pas les types :
container match {
case StringContainer(x) => println(x.toUpperCase)
case DoubleContainer(x) => println(math.sqrt(x))
case _ => println("_")
}
Apparemment, on pourrait définir des méthodes unapply
dans deux objets, StringContainer
et DoubleContainer
et utiliser la même correspondance que ci-dessus, au lieu d'étendre la classe Container
:
case class Container[+A](val value: A)
object StringContainer {
def unapply(c: Container[String]): Option[String] = Some(c.value)
}
object DoubleContainer {
def unapply(c: Container[Double]): Option[Double] = Some(c.value)
}
Mais cela ne fonctionne pas, encore une fois, en raison de l'effacement de type JVM.
Une référence à l'article de Rob Norris, qui m'a conduit à cette réponse, peut être trouvée ici: https://class.coursera.org/progfun-002/forum/thread?thread_id=842#post-3567 . Malheureusement, vous ne pouvez y accéder que si vous êtes inscrit au cours Coursera.
Remarque: vous avez également une alternative avec Miles Sabin s Bibliothèque Shapeless ( déjà mentionné par Miles en 2012 ici ).
Vous pouvez voir un exemple dans " Façons de faire correspondre les modèles aux types génériques dans Scala " de Jaakko Pallari
Typeable
est une classe de type qui permet de convertir des valeurs du typeAny
en un type spécifique .
Le résultat de l'opération de transtypage est unOption
où la valeurSome
contiendra la valeur transtypée avec succès et la valeurNone
représente un échec de transtypage.
TypeCase
pontsTypeable
et correspondance de motifs. C'est essentiellement un extracteur pour les instances deTypeable
import shapeless._
def extractCollection[T: Typeable](a: Any): Option[Iterable[T]] = {
val list = TypeCase[List[T]]
val set = TypeCase[Set[T]]
a match {
case list(l) => Some(l)
case set(s) => Some(s)
case _ => None
}
}
val l1: Any = List(1, 2, 3)
val l2: Any = List[Int]()
val s: Any = Set(1, 2, 3)
extractCollection[Int](l1) // Some(List(1, 2, 3))
extractCollection[Int](s) // Some(Set(1, 2, 3))
extractCollection[String](l1) // None
extractCollection[String](s) // None
extractCollection[String](l2) // Some(List()) // Shouldn't this be None? We'll get back to this.
Bien que
Typeable
puisse sembler avoir ce qu'il faut pour résoudre l'effacement de type, il est toujours soumis au même comportement que tout autre code d'exécution.
Cela peut être vu dans les dernières lignes des exemples de code précédents où les listes vides étaient reconnues comme des listes de chaînes même lorsqu'elles étaient spécifiées comme étant des listes entières. En effet, les transtypagesTypeable
sont basés sur les valeurs de la liste. Si la liste est vide, alors naturellement c'est une liste de chaînes valide et une liste d'entiers valide (ou toute autre liste d'ailleurs)