web-dev-qa-db-ja.com

クリーナータプルgroupBy

キーと値のペア(String、Int)のシーケンスがあり、キーごとに値のシーケンス(Seq[(String, Int)]) => Map[String, Iterable[Int]]))にグループ化したい。

明らかに、ここではtoMapは役に立ちません。また、groupByは値をタプルとして維持します。私が思いついた最高のものは次のとおりです。

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

これを行うためのよりクリーンな方法はありますか?

32
Tomer Gabel

これは、トラバーサブブルにtoMultiMapメソッドを追加するポン引きです。それはあなたの問題を解決しますか?

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)
19

標準ライブラリにはこれを行うためのメソッドやデータ構造はなく、ソリューションはほぼ簡潔に見えます。これを複数の場所で使用する場合は、ユーティリティメソッドに分解することをお勧めします

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

その後、明らかにgroupTuples(seq)で呼び出します。これはCPUクロックサイクルの観点からは最も効率的ではないかもしれませんが、特に非効率的でもないと思います。

9つのタプルのリストでJean-Philippeのソリューションに対して大まかなベンチマークを実行しましたが、これはわずかに高速です。どちらも、シーケンスをマップに折りたたむよりも約2倍高速でした(効果的にgroupByを再実装して、必要な出力を提供します)。

あなたがそれをよりきれいだと考えるかどうかはわかりません:

seq.groupBy(_._1).map { case (k,v) => (k,v.map(_._2))}
8
Johnny Everson

_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は、2番目のタプル部分(List((1,a), (1,b)))を使用して値(List(a, b))をグループ化しました。

0
Xavier Guihot