Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern matching against Scala Map type

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?

like image 793
Tom Morris Avatar asked Nov 23 '12 22:11

Tom Morris


People also ask

Does Scala have pattern matching?

Scala's pattern matching statement is most useful for matching on algebraic types expressed via case classes. Scala also allows the definition of patterns independently of case classes, using unapply methods in extractor objects.

What is the Map type in the Scala?

By default Scala uses immutable Map.

Which method of case class allows using objects in pattern matching?

The match method takes a number of cases as an argument. Each alternative takes a pattern and one or more expressions that will be performed if the pattern matches.

How do you write a match case in Scala?

Using if expressions in case statements i match { case a if 0 to 9 contains a => println("0-9 range: " + a) case b if 10 to 19 contains b => println("10-19 range: " + a) case c if 20 to 29 contains c => println("20-29 range: " + a) case _ => println("Hmmm...") }


3 Answers

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.

like image 156
AmigoNico Avatar answered Oct 11 '22 22:10

AmigoNico


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.

like image 8
DaoWen Avatar answered Oct 11 '22 23:10

DaoWen


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)

like image 6
Guillaume Massé Avatar answered Oct 12 '22 00:10

Guillaume Massé