web-dev-qa-db-fra.com

Comment faire correspondre un modèle sur un type générique dans Scala?

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?

27

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")
}
30
drexin

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")
    }
28
rarry

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.

12

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 type Any en un type spécifique .
Le résultat de l'opération de transtypage est un Option où la valeur Some contiendra la valeur transtypée avec succès et la valeur None représente un échec de transtypage.

TypeCase ponts Typeable et correspondance de motifs. C'est essentiellement un extracteur pour les instances de Typeable

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 transtypages Typeable 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)

5
VonC