可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Imagine I have a Map[String, String]
in Scala.
I want to match against the full set of key–value pairings in the map.
Something like this ought to be 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"
}
The compiler complains thulsy:
error: value Map is not a case class constructor, nor does it have an unapply/unapplySeq method
What currently is the best way to pattern match for key–value combinations in a Map
?
回答1:
Pattern matching is not what you want. You want to find if A fully contains 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 ) )
Edit: adding matching criteria
This way you can add matching criteria easily
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
Gives:
List(chinese restaurant, che che)
回答2:
You could use flatMap
to pull out the values you are interested in and then match against them:
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"
}
See #1 on this snippets page.
You can check whether an arbitrary list of keys have particular values like so:
if ( ( keys flatMap ( record get _ ) ) == values ) ...
Note that the above works even if keys can be absent from the map, but if the keys share some values you probably want to use map
instead of flatMap
and be explicit with Some
/None
in your list of values. E.g. in this case if "amenity" might be absent and the value of "cuisine" might be "restaurant" (silly for this example, but perhaps not in another context), then case "restaurant"::_
would be ambiguous.
Also, it is worth noting that case "restaurant"::"chinese"::_
is slightly more efficient than case List("restaurant","chinese")
because the latter needlessly checks that there are no more elements after those two.
回答3:
You could just look up the values in question, stick them in a tuple, and pattern match on that:
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"
}
Or, you could do some nested matches, which might be a bit cleaner:
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"
}
Note that map.get(key)
returns an Option[ValueType]
(in this case ValueType would be String), so it will return None
rather than throwing an exception if the key doesn't exist in the map.
回答4:
I find the following solution using extractors the most similar to case classes. It's mostly syntactic gravy though.
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")
}
回答5:
Another version which requires you to specify the keys you want to extract and allows you to match on the values is the following:
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")
}
回答6:
Because, despite agreeing that all the other answers are very sensible, I was interested to see if there was in fact a way to pattern-match using maps, I put together the following. It uses the same logic as the top answer to determine a match.
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.