Более чистая группа кортежейОт

У меня есть последовательность пар ключ-значение (String, Int), и я хочу сгруппировать их по ключу в последовательность значений (т.е. Seq[(String, Int)]) => Map[String, Iterable[Int]])).

Очевидно, что toMap здесь бесполезна, а groupBy поддерживает значения в виде кортежей. Лучшее, что мне удалось придумать, это:

val seq: Seq[( String, Int )]
// ...
seq.groupBy( _._1 ).mapValues( _.map( _._2 ) )

Есть ли более чистый способ сделать это?


person Tomer Gabel    schedule 28.05.2012    source источник
comment
+1 Используйте этот шаблон так часто, что я бы хотел, чтобы что-то было встроено.   -  person Garrett Hall    schedule 24.07.2013


Ответы (4)


Вот сутенер, который добавляет метод toMultiMap к traversables. Решит ли это вашу проблему?

import collection._
import mutable.Builder
import generic.CanBuildFrom

class TraversableOnceExt[CC, A](coll: CC, asTraversable: CC => TraversableOnce[A]) {

  def toMultiMap[T, U, That](implicit ev: A <:< (T, U), cbf: CanBuildFrom[CC, U, That]): immutable.Map[T, That] =
    toMultiMapBy(ev)

  def toMultiMapBy[T, U, That](f: A => (T, U))(implicit cbf: CanBuildFrom[CC, U, That]): immutable.Map[T, That] = {
    val mutMap = mutable.Map.empty[T, mutable.Builder[U, That]]
    for (x <- asTraversable(coll)) {
      val (key, value) = f(x)
      val builder = mutMap.getOrElseUpdate(key, cbf(coll))
      builder += value
    }
    val mapBuilder = immutable.Map.newBuilder[T, That]
    for ((k, v) <- mutMap)
      mapBuilder += ((k, v.result))
    mapBuilder.result
  }
}

implicit def commomExtendTraversable[A, C[A] <: TraversableOnce[A]](coll: C[A]): TraversableOnceExt[C[A], A] =
  new TraversableOnceExt[C[A], A](coll, identity)

Что можно использовать так:

val map = List(1 -> 'a', 1 -> 'à', 2 -> 'b').toMultiMap
println(map)  // Map(1 -> List(a, à), 2 -> List(b))

val byFirstLetter = Set("abc", "aeiou", "cdef").toMultiMapBy(elem => (elem.head, elem))
println(byFirstLetter) // Map(c -> Set(cdef), a -> Set(abc, aeiou))

Если вы добавите следующие неявные определения, они также будут работать с объектами, подобными коллекциям, такими как Strings и Arrays:

implicit def commomExtendStringTraversable(string: String): TraversableOnceExt[String, Char] =
  new TraversableOnceExt[String, Char](string, implicitly)

implicit def commomExtendArrayTraversable[A](array: Array[A]): TraversableOnceExt[Array[A], A] =
  new TraversableOnceExt[Array[A], A](array, implicitly)

Затем:

val withArrays = Array(1 -> 'a', 1 -> 'à', 2 -> 'b').toMultiMap
println(withArrays) // Map(1 -> [C@377653ae, 2 -> [C@396fe0f4)

val byLowercaseCode = "Mama".toMultiMapBy(c => (c.toLower.toInt, c))
println(byLowercaseCode) // Map(97 -> aa, 109 -> Mm)
person Jean-Philippe Pellet    schedule 28.05.2012
comment
Гораздо больше, чем я рассчитывал, но, тем не менее, полезно. Спасибо! - person Tomer Gabel; 29.05.2012
comment
Это очень мило. Есть ли простой способ переопределить тип коллекции значений (например, у меня есть List(1 -> 'a', 1 -> 'à', 2 -> 'b'), но я хочу, чтобы результатом toMultiMap было Map[Int, Set[String]]? Возможно, какой-то трюк с breakOut? - person Tomáš Dvořák; 11.11.2016

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

def groupTuples[A, B](seq: Seq[(A, B)]) = 
  seq groupBy (_._1) mapValues (_ map (_._2))

который вы затем, очевидно, просто вызываете с помощью groupTuples(seq). Возможно, это не самый эффективный вариант с точки зрения тактовых циклов процессора, но я не думаю, что он особенно неэффективен.

Я сделал грубый тест на решение Жана-Филиппа в списке из 9 кортежей, и это немного быстрее. Оба были примерно в два раза быстрее, чем складывание последовательности в карту (эффективно повторно реализовав groupBy для получения желаемого результата).

person Luigi Plinge    schedule 28.05.2012
comment
mapValues на самом деле просто оборачивает построенную карту, поэтому он может стать менее эффективным при поиске вещей на карте. Кроме того, я отредактировал свой ответ, чтобы избежать toMap; Не могли бы вы (из любопытства) снова запустить тот же тест? С 9 кортежами построение карты и два поиска примерно на треть быстрее с моим предложением в соответствии с моими тестами. - person Jean-Philippe Pellet; 29.05.2012
comment
@ Жан-Филипп Я получаю 97 мс за 100 тыс. прогонов для вышеперечисленного и 106 мс с вашим обновленным. Конечно, мы действительно должны попробовать разные длины списков и композиции, но я просто хотел получить примерное представление. С практической точки зрения они имеют одинаковую скорость. - person Luigi Plinge; 29.05.2012
comment
@ Jean-Philippe Интересно насчет mapValues упаковки существующей карты - я этого не знал. Создание совершенно новой карты с помощью seq groupBy (_._1) map (x => (x._1, x._2 map (_._2))) занимает 165 мс, поэтому для создания новой карты в памяти у вас это получается быстрее. - person Luigi Plinge; 29.05.2012
comment
Эффективность на самом деле не была большой проблемой, но приятно знать эффект независимо от этого. Жаль, что я не могу принять два ответа - мне придется пойти с ответом Жана-Филиппа на том простом основании, что он настолько всеобъемлющий :-) - person Tomer Gabel; 29.05.2012

Я не знаю, считаете ли вы это более чистым:

seq.groupBy(_._1).map { case (k,v) => (k,v.map(_._2))}
person Johnny Everson    schedule 28.05.2012
comment
Я не знаю, правда. Искал более лаконичное и/или семантически понятное решение. - person Tomer Gabel; 31.05.2012
comment
Конечно. Я думаю, вам придется создать для этого функцию и вызывать ее при необходимости. таким образом вы можете сделать синтаксис таким, каким хотите. - person Johnny Everson; 31.05.2012

Начиная с Scala 2.13, большинство коллекций поставляются с groupMap, который (как его имя предлагает) эквивалент (более эффективный) groupBy, за которым следует mapValues:

List(1 -> 'a', 1 -> 'b', 2 -> 'c').groupMap(_._1)(_._2)
// Map[Int,List[Char]] = Map(2 -> List(c), 1 -> List(a, b))

Этот:

  • groups элементов на основе первой части кортежей (Map(2 -> List((2,c)), 1 -> List((1,a), (1,b))))

  • maps сгруппировал значения (List((1,a), (1,b))), взяв их вторую часть кортежа (List(a, b)).

person Xavier Guihot    schedule 02.10.2018