Например, предположим, что
val list: List[(String, Double)]
со значениями
"04-03-1985", 1.5
"05-03-1985", 2.4
"05-03-1985", 1.3
Как я могу создать новый список
"04-03-1985", 1.5
"05-03-1985", 3.7
Например, предположим, что
val list: List[(String, Double)]
со значениями
"04-03-1985", 1.5
"05-03-1985", 2.4
"05-03-1985", 1.3
Как я могу создать новый список
"04-03-1985", 1.5
"05-03-1985", 3.7
Здесь один лайнер. Это не особенно читаемо, если только он действительно не усваивает типы этих функций более высокого порядка.
val s = Seq(("04-03-1985" -> 1.5),
("05-03-1985" -> 2.4),
("05-03-1985" -> 1.3))
s.groupBy(_._1).mapValues(_.map(_._2).sum)
// returns: Map(04-03-1985 -> 1.5, 05-03-1985 -> 3.7)
Другим подходом является добавление пар ключ-значение один за другим с помощью fold,
s.foldLeft(Map[String, Double]()) { case (m, (k, v)) =>
m + (k -> (v + m.getOrElse(k, 0d)))
}
Эквивалент для понимания наиболее доступен, на мой взгляд,
var m = Map[String, Double]()
for ((k, v) <- s) {
m += k -> (v + m.getOrElse(k, 0d))
}
Может быть, что-то более приятное можно сделать с помощью Scolaz monoid typeclass для Map.
Обратите внимание, что вы можете конвертировать между Map[K, V]
и Seq[(K, V)]
с помощью методов toSeq
и toMap
.
Update. Поразмыслив над этим, я думаю, что естественная абстракция будет "многомассовым" преобразованием типа
def seqToMultimap[A, B](s: Seq[(A, B)]): Map[A, Seq[B]]
При соответствующем неявном расширении в одной личной библиотеке можно было бы написать:
s.toMultimap.mapValues(_.sum)
Это самое лучшее, на мой взгляд!
Существует еще одна возможность использования Scalaz.
Ключевым моментом является заметить, что если M
является Monoid
, то Map[T, M]
также является Monoid
. Это означает, что если у меня есть 2 карты, m1
и m2
, я могу добавить их так, чтобы для каждого подобного ключа элементы были добавлены вместе.
Например, Map[String, List[String]]
является моноидом, потому что List[String]
является Monoid
. Поэтому, учитывая соответствующий экземпляр Monoid
в области видимости, я должен уметь:
val m1 = Map("a" -> List(1), "b" -> List(3))
val m2 = Map("a" -> List(2))
// |+| "adds" two elements of a Monoid together in Scalaz
m1 |+| m2 === Map("a" -> List(1, 2), "b" -> List(3))
По вашему вопросу мы можем видеть, что Map[String, Int]
является Monoid
, потому что существует экземпляр Monoid
для типа Int
. Позвольте импортировать его:
implicit val mapMonoid = MapMonoid[String, Int]
Тогда мне нужна функция reduceMonoid
, которая берет что-нибудь, что Traversable
и "добавляет" свои элементы с помощью Monoid
. Я просто пишу определение reduceMonoid
здесь, для полной реализации, см. Мой пост в Суть шаблона итератора:
// T is a "Traversable"
def reduce[A, M : Monoid](reducer: A => M): T[A] => M
Эти 2 определения не существуют в текущей библиотеке Scalaz, но их нетрудно добавить (на основе существующих классов Monoid
и Traverse
). И как только мы их получим, решение вашего вопроса очень просто:
val s = Seq(("04-03-1985" -> 1.5),
("05-03-1985" -> 2.4),
("05-03-1985" -> 1.3))
// we just put each pair in its own map and we let the Monoid instance
// "add" the maps together
s.reduceMonoid(Map(_)) === Map("04-03-1985" -> 1.5,
"05-03-1985" -> 3.7)
Если вы считаете, что приведенный выше код немного неясен (но очень краткий, верно?), я рекомендую вам проверить проект github для сообщения EIP и играть с этим. В одном примере показано решение вашего вопроса:
"I can build a map String->Int" >> {
val map1 = List("a" -> 1, "a" -> 2, "b" -> 3, "c" -> 4, "b" -> 5)
implicit val mapMonoid = MapMonoid[String, Int]
map1.reduceMonoid(Map(_)) must_== Map("a" -> 3, "b" -> 8, "c" -> 4)
}
Я использовал этот шаблон s.groupBy(_._1).mapValues(_.map(_._2).sum)
от Kipton все время. Это очень хорошо отражает мой мыслительный процесс, но, к сожалению, не всегда легко читать. Я обнаружил, что использование класса case по возможности делает вещи немного лучше:
case class Data(date: String, amount: Double)
val t = s.map(t => (Data.apply _).tupled(t))
// List(Data(04-03-1985,1.5), Data(05-03-1985,2.4), Data(05-03-1985,1.3))
Затем он становится:
t.groupBy(_.date).mapValues{ group => group.map(_.amount).sum }
// Map(04-03-1985-> 1.5, 05-03-1985 -> 3.7)
Я думаю, что тогда это более читаемо, чем складка или версия.
val s = List ( "04-03-1985" -> 1.5, "05-03-1985" -> 2.4, "05-03-1985" -> 1.3)
for { (key, xs) <- s.groupBy(_._1)
x = xs.map(_._2).sum
} yield (key, x)
Начиная с Scala 2.13
, вы можете использовать метод K)(f:A=>B)(reduce:(B,B)=>B):scala.collection.immutable.Map[K,B] rel="nofollow noreferrer"> groupMapReduce
который (как следует из его названия) эквивалентен groupBy
за которым следует mapValues
и шаг reduce
:
// val l = List(("04-03-1985", 1.5), ("05-03-1985", 2.4), ("05-03-1985", 1.3))
l.groupMapReduce(_._1)(_._2)(_ + _).toList
// List(("04-03-1985", 1.5), ("05-03-1985", 3.7))
Это:
group
кортежи по первой части (_._1
) (групповая часть группы MapReduce)
map
каждый сгруппированный кортеж со своей второй частью (_._2
) (отобразить часть группы Map Reduce)
reduce
значения в каждой группе (_ + _
), суммируя их (уменьшить часть groupMap Reduce).
Это однопроходная версия того, что можно перевести:
l.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _)).toList