Pattern matching against Scala Map type

34,893

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")
}
Share:
34,893

Related videos on Youtube

Tom Morris
Author by

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, 2020

Comments

  • Tom Morris
    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é
    Guillaume Massé over 11 years
    like the answer from DaoWen, you cannot take an arbitrary value to match against.
  • AmigoNico
    AmigoNico over 11 years
    I don't understand, Guillaume -- can you elaborate?
  • AmigoNico
    AmigoNico over 11 years
    OK, 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 and values as arguments and do the if as above.