web-dev-qa-db-fra.com

Correspondance de modèle avec Scala Type de carte

Imaginez que j'ai un Map[String, String] dans Scala.

Je souhaite effectuer une comparaison avec l'ensemble complet des paires clé-valeur de la carte.

Quelque chose comme ça devrait être possible

val record = Map("amenity" -> "restaurant", "cuisine" -> "chinese", "name" -> "Golden Palace")
record match {
    case Map("amenity" -> "restaurant", "cuisine" -> "chinese") => "a Chinese restaurant"
    case Map("amenity" -> "restaurant", "cuisine" -> "italian") => "an Italian restaurant"
    case Map("amenity" -> "restaurant") => "some other restaurant"
    case _ => "something else entirely"
}

Le compilateur se plaint Thulsy:

error: value Map is not a case class constructor, nor does it have an unapply/unapplySeq method

Quelle est actuellement la meilleure façon de mettre en correspondance des modèles pour les combinaisons clé-valeur dans un Map?

21
Tom Morris

La correspondance de modèles n'est pas ce que vous voulez. Vous voulez savoir si A contient entièrement B

val record = Map("amenity" -> "restaurant", "cuisine" -> "chinese", "name" -> "Golden Palace")
val expect = Map("amenity" -> "restaurant", "cuisine" -> "chinese")
expect.keys.forall( key => expect( key ) == record( key ) )

Modifier: ajouter des critères de correspondance

De cette façon, vous pouvez facilement ajouter des critères de correspondance

val record = Map("amenity" -> "restaurant", "cuisine" -> "chinese", "name" -> "Golden Palace")

case class FoodMatcher( kv: Map[String,String], output: String )

val matchers = List( 
    FoodMatcher(  Map("amenity" -> "restaurant", "cuisine" -> "chinese"), "chinese restaurant, che che" ),
    FoodMatcher(  Map("amenity" -> "restaurant", "cuisine" -> "italian"), "italian restaurant, mama mia" )
)

for {
    matcher <- matchers if matcher.kv.keys.forall( key => matcher.kv( key ) == record( key ) )
} yield matcher.output

Donne:

List(chinese restaurant, che che)

6
Guillaume Massé

Vous pouvez utiliser flatMap pour extraire les valeurs qui vous intéressent et les comparer:

List("amenity","cuisine") flatMap ( record get _ ) match {
  case "restaurant"::"chinese"::_ => "a Chinese restaurant"
  case "restaurant"::"italian"::_ => "an Italian restaurant"
  case "restaurant"::_            => "some other restaurant"
  case _                          => "something else entirely"
}

Voir # 1 sur cette page d'extraits de code .

Vous pouvez vérifier si une liste arbitraire de clés a des valeurs particulières comme ceci :

if ( ( keys flatMap ( record get _ ) ) == values ) ...

Notez que ce qui précède fonctionne même si les clés peuvent être absentes de la carte, mais si les clés partagent certaines valeurs, vous voudrez probablement utiliser map au lieu de flatMap et être explicite avec Some/None dans votre liste de valeurs. Par exemple. dans ce cas, si "agrément" peut être absent et que la valeur de "cuisine" peut être "restaurant" (idiot dans cet exemple, mais peut-être pas dans un autre contexte), alors case "restaurant"::_ serait ambigu.

De plus, il est à noter que case "restaurant"::"chinese"::_ Est légèrement plus efficace que case List("restaurant","chinese") car ce dernier vérifie inutilement qu'il n'y a plus d'éléments après ces deux.

11
AmigoNico

Je trouve que la solution suivante utilisant des extracteurs est la plus similaire aux classes de cas. C'est surtout de la sauce syntaxique.

object Ex {
   def unapply(m: Map[String, Int]) : Option[(Int,Int) = for {
       a <- m.get("A")
       b <- m.get("B")
   } yield (a, b)
}

val ms = List(Map("A" -> 1, "B" -> 2),
    Map("C" -> 1),
    Map("C" -> 1, "A" -> 2, "B" -> 3),
    Map("C" -> 1, "A" -> 1, "B" -> 2)
    )  

ms.map {
    case Ex(1, 2) => println("match")
    case _        => println("nomatch")
}
2
Joerg Schmuecker

Une autre version qui vous oblige à spécifier les clés que vous souhaitez extraire et vous permet de faire correspondre les valeurs est la suivante:

class MapIncluding[K](ks: K*) {
  def unapplySeq[V](m: Map[K, V]): Option[Seq[V]] = if (ks.forall(m.contains)) Some(ks.map(m)) else None
}

val MapIncludingABC = new MapIncluding("a", "b", "c")
val MapIncludingAAndB = new MapIncluding("a", "b")

Map("a" -> 1, "b" -> 2) match {
  case MapIncludingABC(a, b, c) => println("Should not happen")
  case MapIncludingAAndB(1, b) => println(s"Value of b inside map is $b")
}
2
Nightscape

Parce que, bien que d'accord pour dire que toutes les autres réponses sont très raisonnables, j'étais intéressé de voir s'il y avait en fait un moyen de faire correspondre les motifs à l'aide de cartes, j'ai rassemblé ce qui suit. Il utilise la même logique que la première réponse pour déterminer une correspondance.

class MapSubsetMatcher[Key, Value](matcher: Map[Key, Value]) {
  def unapply(arg: Map[Key, Value]): Option[Map[Key, Value]] = {
    if (matcher.keys.forall(
      key => arg.contains(key) && matcher(key) == arg(key)
    ))
      Some(arg)
    else
      None
  }
}

val chineseRestaurant = new MapSubsetMatcher(Map("amenity" -> "restaurant", "cuisine" -> "chinese"))
val italianRestaurant = new MapSubsetMatcher(Map("amenity" -> "restaurant", "cuisine" -> "italian"))
val greatPizza = new MapSubsetMatcher(Map("pizza_rating" -> "excellent"))

val record = Map("amenity" -> "restaurant", "cuisine" -> "chinese", "name" -> "Golden Palace")
val frankies = Map("amenity" -> "restaurant", "cuisine" -> "italian", "name" -> "Frankie's", "pizza_rating" -> "excellent")


def matcher(x: Any): String = x match {
  case greatPizza(_) => "It's really good, you should go there."
  case chineseRestaurant(matchedMap) => "a Chinese restaurant called " +
    matchedMap.getOrElse("name", "INSERT NAME HERE")
  case italianRestaurant(_) => "an Italian restaurant"
  case _ => "something else entirely"
}

matcher(record)
// a Chinese restaurant called Golden Palace
matcher(frankies)
// It's really good, you should go there.
1
Pete Wildsmith