2012-11-23 12 views
16

Immagina di avere un Map[String, String] in Scala.Corrispondenza di modelli con Scala Tipo di mappa

Voglio confrontarmi con il set completo di coppie di valori-chiave nella mappa.

Qualcosa del genere dovrebbe essere possibile

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

Il compilatore si lamenta thulsy:

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

Qual è attualmente il modo migliore per corrispondenza modello per combinazioni di valori-chiave in un Map?

risposta

2

L'abbinamento del motivo non è quello che desideri. Si vuole trovare se A contiene pienamente 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)) 

Modifica: aggiungendo coincidenti

In questo modo è possibile aggiungere i criteri corrispondenti facilmente

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 

Dà:

List(chinese restaurant, che che)

6

Si può solo cercare i valori in questione, li bastone in una tupla, e pattern match su questo:

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

Oppure, si potrebbe fare alcune partite nidificate, che potrebbe essere un po 'più pulito:

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

noti che map.get(key) restituisce un Option[ValueType] (in questo caso ValueType sarebbe String), in modo che tornerà None piuttosto che un'eccezione se la chiave non esiste nella mappa.

+0

La soluzione corrispondente nidificato sembra piuttosto buono. –

8

È possibile utilizzare flatMap per estrarre i valori che si sono interessati e poi partita contro di loro:

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

Vedi # 1 su this snippets page.

È possibile controllare se un elenco arbitrario di chiavi presenta particolari valori in questo modo:

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

Si noti che quanto sopra funziona anche se i tasti possono essere assenti dalla mappa, ma se la quota di chiavi alcuni valori probabilmente si desidera utilizzare map anziché flatMap ed essere espliciti con Some/None nell'elenco dei valori. Per esempio. in questo caso se "l'amenità" potrebbe essere assente e il valore di "cucina" potrebbe essere "ristorante" (sciocco per questo esempio, ma forse non in un altro contesto), quindi case "restaurant"::_ sarebbe ambiguo.

Inoltre, vale la pena notare che case "restaurant"::"chinese"::_ è leggermente più efficiente di case List("restaurant","chinese") perché quest'ultimo verifica inutilmente che non ci sono più elementi dopo questi due.

+0

come la risposta di DaoWen, non è possibile assumere un valore arbitrario in modo che corrisponda. –

+0

Non capisco, Guillaume - puoi elaborare? – AmigoNico

+0

dai un'occhiata al mio edit –

2

Trovo la seguente soluzione utilizzando gli estrattori più simili alle classi dei casi. È per lo più sugo sintattico però.

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

Perché, nonostante concordando sul fatto che tutte le altre risposte sono molto sensibile, ero curioso di vedere se ci fosse in realtà un modo per pattern-partita utilizzando mappe, ho messo insieme quanto segue. Utilizza la stessa logica della risposta superiore per determinare una corrispondenza.

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. 
0

Un'altra versione che richiede di specificare le chiavi che si desidera estrarre e consente di adattare ai valori è la seguente:

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") 
} 
Problemi correlati