Scala: построение карты из списка кортежей, но сбой при наличии противоречивых записей

#scala #collections

#scala #Коллекции

Вопрос:

Я думаю, что это может быть обычная операция. Так что, возможно, это внутри API, но я не могу его найти. Также меня интересует эффективное функциональное / простое решение, если нет.

Учитывая последовательность кортежей ("a" -> 1, "b" ->2, "c" -> 3) , я хочу превратить это в карту. Это простое использование TraversableOnce.toMap . Но я хочу потерпеть неудачу в этой конструкции, если результирующая карта «будет содержать противоречие», то есть разные значения, присвоенные одному и тому же ключу. Как в последовательности ("a" -> 1, "a" -> 2) . Но дубликаты должны быть разрешены.

В настоящее время у меня есть этот (очень важный) код:

 def buildMap[A,B](in: TraversableOnce[(A,B)]): Option[Map[A,B]] = {
  val map = new HashMap[A,B]
  val it = in.toIterator
  var fail = false
  while(it.hasNext){
    val next = it.next()
    val old = map.put(next._1, next._2)
    fail = old.isDefined amp;amp; old.get != next._2
  }

  if(fail) None else Some(map.toMap)
}
  

Побочный вопрос

Действительно ли необходим final toMap ? Я получаю ошибку типа при ее пропуске, но я думаю, что это должно сработать. Реализация toMap создает новую карту, которой я хочу избежать.

Комментарии:

1. Как вы хотите, чтобы выглядел сбой? (например, вы хотите, чтобы была возвращена опция [Map]?)

Ответ №1:

Как всегда при работе с Seq[A] оптимальным решением, производительность зависит от конкретного типа коллекции. Общим, но не очень эффективным решением было бы свернуть Option[Map[A,B]] :

 def optMap[A,B](in: Iterable[(A,B)]): Option[Map[A,B]] = 
  in.iterator.foldLeft(Option(Map[A,B]())) {
    case (Some(m),e @ (k,v)) if m.getOrElse(k, v) == v => Some(m   e)
    case _ => None
  }
  

Если вы ограничитесь использованием List[A,B] s, оптимизированная версия будет:

 @tailrec
def rmap[A,B](in: List[(A,B)], out: Map[A,B] = Map[A,B]()): Option[Map[A,B]] = in match {
  case (e @ (k,v)) :: tail if out.getOrElse(k,v) == v =>
    rmap(tail, out   e)
  case Nil =>
    Some(out)
  case _ => None
}
  

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

 def mmap[A,B](in: Iterable[(A,B)]): Option[Map[A,B]] = {
  val dest = collection.mutable.Map[A,B]()

  for (e @ (k,v) <- in) {
    if (dest.getOrElse(k, v) != v) return None
    dest  = e
  }

  Some(dest.toMap)
}
  

Комментарии:

1. Я думаю, использование getOrElseUpdate может сделать ваш третий код немного короче.

2. Ваш «менее идиоматичный» вариант кажется мне значительно более понятным. Мне было бы интересно услышать от читателей с большим опытом функционального программирования, чем у меня, найдут ли они, что разобраться с тем, что делает ваш первый, проще, чем делать то же самое для последнего…

3. @ziggystar, короче, но побочный эффект в условии означает менее понятный (для меня, в любом случае)

Ответ №2:

Вот решение, которое не приводит к сбоям (если создание всей карты, а затем удаление ее допустимо):

 def uniqueMap[A,B](s: Seq[(A,B)]) = {
  val m = s.toMap
  if (m.size == s.length) Some(s) else None
}
  

Вот изменяемое решение без сбоев (отключается при обнаружении ошибки):

 def uniqueMap[A,B](s: Seq[(A,B)]) = {
  val h = new collection.mutable.HashMap[A,B]
  val i = s.iterator.takeWhile(x => !(h contains x._1)).foreach(h  = _)
  if (h.size == s.length) Some(h) else None
}
  

И вот неизменяемое безотказное решение:

 def uniqueMap[A,B](s: Seq[(A,B)]) = {
  def mapUniquely(i: Iterator[(A,B)], m: Map[A,B]): Option[Map[A,B]] = {
    if (i.hasNext) {
      val j = i.next
      if (m contains j._1) None
      else mapUniquely(i, m   j)
    }
    else Some(m)
  }
  mapUniquely(s.iterator, Map[A,B]())
}
  

Редактировать: и вот решение, использующее put для ускорения (надеюсь):

 def uniqueMap[A,B](s: Seq[(A,B)]) = {
  val h = new collection.mutable.HashMap[A,B]
  val okay = s.iterator.forall(x => {
    val y = (h put (x._1,x._2))
    y.isEmpty || y.get == x._2 
  })
  if (okay) Some(h) else None
}
  

Редактировать: теперь протестировано, и это работает в ~ 2 раза быстрее при вводе (возвращает true), чем Мориц или мое простое решение.

Комментарии:

1. Я хочу потерпеть неудачу только при противоречивых дубликатах, а не при точных дубликатах. Тем не менее, очень хороший ответ, и я могу легко адаптировать его к моему случаю. Мне нравится, как вы используете takeWhile и foreach в нелогичной последовательности. 🙂 Я буду придерживаться второго предложения по соображениям производительности.

2. @ziggystar — Моя ошибка. Замените h contains x._1 на h.get(x._1).map(_ == x._2).getOrElse(true) или что-то подобное, и все будет готово.

3. Адаптация не так проста. Проблема в том, что я, вероятно, захочу использовать HashMap.put, который возвращает старый элемент, который я затем могу сравнить с новым. Но при использовании вашего решения это невозможно. Мне пришлось бы добавить элемент, а затем проверить условие, выполнив второй запрос к карте.

4. @ziggystar — Придирчивый, придирчивый! Хорошо, я обновлю ответ, который работает там.

Ответ №3:

Scala 2.9 уже близко, так почему бы не воспользоваться методом комбинаций (вдохновленным ответом Морица):

 def optMap[A,B](in: List[(A,B)]) = {
  if (in.combinations(2).exists { 
    case List((a,b),(c,d)) => a == c amp;amp; b != d
    case _ => false 
  }) None else Some(in.toMap)
}

scala> val in = List(1->1,2->3,3->4,4->5,2->3)
in: List[(Int, Int)] = List((1,1), (2,3), (3,4), (4,5), (2,3))

scala> optMap(in)
res29: Option[scala.collection.immutable.Map[Int,Int]] = Some(Map(1 -> 1, 2 -> 3, 3 -> 4, 4 -> 5))

scala> val in = List(1->1,2->3,3->4,4->5,2->3,1->2)
in: List[(Int, Int)] = List((1,1), (2,3), (3,4), (4,5), (2,3), (1,2))

scala> optMap(in)
res30: Option[scala.collection.immutable.Map[Int,Int]] = None
  

Комментарии:

1. Вау, это удивительно неэффективно.

2. Добро пожаловать в Scala, язык, наиболее простой для написания УДИВИТЕЛЬНО неэффективного кода с минимально возможным объемом

Ответ №4:

Вы также можете использовать gourpBy следующим образом:

   val pList = List(1 -> "a", 1 -> "b", 2 -> "c", 3 -> "d")

  def optMap[A,B](in: Iterable[(A,B)]): Option[Map[A,B]] = {
    Option(in.groupBy(_._1).map{case(_, list) => if(list.size > 1) return None else list.head})
  }

  println(optMap(pList))
  

По эффективности это не уступает вышеупомянутым решениям.
На самом деле, если вы изучите реализацию gourpBy, вы увидите, что она очень похожа на некоторые из предложенных решений.