Pattern matching against Scala Map type
Solution 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)
Solution 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.
Solution 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.
Solution 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")
}
Solution 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")
}
Related videos on Youtube
Tom Morris
Code in Scala, Ruby, Rails, Python (and Java if necessary). Use Vim, TextMate, IntelliJ, Eclipse and NetBeans (in that order). Very interested in geodata and RDF. Have helped organise BarCampLondon and a few hack days. Use OS X and Debian/Ubuntu (anything with apt), stay well clear of Windows. Twitter: http://twitter.com/tommorris
Updated on August 09, 2020Comments
-
Tom Morris over 3 years
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
? -
Guillaume Massé over 11 yearslike the answer from DaoWen, you cannot take an arbitrary value to match against.
-
AmigoNico over 11 yearsI don't understand, Guillaume -- can you elaborate?
-
AmigoNico over 11 yearsOK, I think I understand what you are saying. Let's say the PartialFunction is an argument rather than inline code, and you don't know a priori which keys it depends on. Then you wouldn't know what to flatMap over. In that case you wouldn't use PartialFunctions; you would take
keys
andvalues
as arguments and do theif
as above.