Сравнение шаблонов с Scala Тип карты

Предположим, что у меня есть Map[String, String] в Scala.

Я хочу совместить с полным набором пар значений ключа на карте.

Что-то вроде этого должно быть возможно

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

Компилятор жалуется на thulsy:

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

В настоящее время лучший способ сопоставления шаблонов для комбинаций ключевых значений в Map?

Ответы

Ответ 1

Совпадение шаблонов не то, что вы хотите. Вы хотите найти, если A полностью содержит 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 ) )

Изменить: добавление критериев соответствия

Таким образом, вы можете легко добавить критерии соответствия.

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

дает:

List(chinese restaurant, che che)

Ответ 2

Вы можете использовать flatMap, чтобы вытащить интересующие вас значения и затем сопоставить их:

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

См. # 1 на этой странице фрагментов.

Вы можете проверить, имеют ли произвольный список ключей такие значения:

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

Обратите внимание, что вышеизложенное работает, даже если ключи могут отсутствовать на карте, но если ключи разделяют некоторые значения, вы, вероятно, захотите использовать map вместо flatMap и быть явным с Some/None в ваш список значений. Например. в этом случае, если "удобство" может отсутствовать, а ценность "кухни" может быть "рестораном" (глупым для этого примера, но, возможно, не в другом контексте), тогда case "restaurant"::_ будет неоднозначным.

Кроме того, стоит отметить, что case "restaurant"::"chinese"::_ немного более эффективен, чем case List("restaurant","chinese"), потому что последний безошибочно проверяет, что после этих двух элементов больше нет элементов.

Ответ 3

Вы можете просто взглянуть на значения, о которых идет речь, вставить их в кортеж и сопоставить шаблон:

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

Или вы можете сделать несколько вложенных совпадений, которые могут быть немного чище:

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

Обратите внимание, что map.get(key) возвращает Option[ValueType] (в этом случае ValueType будет String), поэтому он вернет None вместо того, чтобы бросать исключение, если ключ не существует на карте.

Ответ 4

Я нахожу следующее решение, используя экстракторы, наиболее похожие на классы case. В основном это синтаксический соус.

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

Потому что, несмотря на согласие с тем, что все другие ответы очень разумны, мне было интересно узнать, действительно ли был способ сопоставления шаблонов с использованием карт, я собрал следующее. Он использует ту же логику, что и главный ответ для определения соответствия.

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 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 really good, you should go there.

Ответ 6

Другая версия, которая требует указать ключи, которые вы хотите извлечь, и позволяет вам сопоставлять значения, следующая:

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