ListのgroupBy、groupMap、groupMapReduceメソッドの使い方

Scala 3 (Dotty 0.26.0-RC1) 2.13.3 2.12.12
最終更新:2020年6月30日

[AD] scalapediaでは記事作成ボランティアを募集しています

この記事では、Scalaの scala.collection.immutable.List クラスにおいて、要素を特定の基準で分類して処理する方法をご紹介します。

要素を分類して処理するには groupBygroupMapgroupMapReduce メソッドを使うことができます。

以下のような、生徒を表す Student クラスを例として用いて説明します。

case class Student(name: String, grade: Int) val students = Seq( Student("John", 1), Student("Mary", 3), Student("Chris", 2), Student("Bob", 3), Student("Nick", 1), Student("Daisy", 1), Student("Rod", 2), Student("Watson", 3), Student("Andy", 2) )

Student クラスは、生徒の名前と学年を保持しています。
生徒の一覧として変数 students を用意しました。

それでは、このサンプルを分類して処理してみましょう。

groupByメソッドを使用して、リストの要素を特定の基準で分類する

リストの要素を特定の基準で分類するには、groupByメソッドを使用します。

def groupBy[K](f: (A) => K): immutable.Map[K, C]

引数として f: (A) => K を受け取ります。
この引数ではリストの要素の何を元にして分類するかを示してあげます。

戻り値として immutable.Map[K, C] を返します。 これは分類した基準値をキー(K)とし、当てはまる要素のコレクションを値(C)とするマップです。

サンプルコードはこちらです。

for { (grade, list) <- students.groupBy(_.grade) str = list.map(_.name).mkString(", ") } println(s"$grade -> $str")

このコードでは、生徒を学年で分類し、名前を文字列としてまとめて出力しています。

出力は以下のようになります。

1 -> John, Nick, Daisy 2 -> Chris, Rod, Andy 3 -> Mary, Bob, Watson

生徒が学年ごとにコンマ区切りで出力されています。 各行の順番は入れ替わることがあることに注意してください。

2.13 で追加された便利なメソッドを使用する

Scala 2.13では、groupMapgroupMapReduce という、さらに便利なメソッドが追加されました。

これらのメソッドを使用すると、先ほどの例をよりシンプルに記述し、しかも効率よく処理することができます。

早速それぞれのメソッドについて見てみましょう。

groupMapメソッドを使用して、リストの要素を特定の基準で分類してそれぞれ処理する

リストの要素を特定の基準で分類してそれぞれ処理するには、groupMapメソッドを使用します。

def groupMap[K, B](key: (A) => K)(f: (A) => B): immutable.Map[K, CC[B]]

引数として key: (A) => Kf: (A) => B の2つを受け取ります。

第一引数はgroupByと同様に、リストの要素の何を元にして分類するかを示してあげます。

第二引数では map メソッドと同様に、分類後のグループのそれぞれの要素をどう処理するかを示してあげます。

戻り値として immutable.Map[K, CC[B]] を返します。 これは分類した基準値をキー(K)とし、当てはまる要素をそれぞれ処理した結果のコレクションを値(CC[B])とするマップです。

サンプルコードはこちらです。

for { (grade, group) <- students.groupMap(_.grade)(_.name) str = group.mkString(", ") } println(s"$grade -> $str")

このコードでは先ほどの例と同様に、生徒を学年によって分類し、名前を文字列としてまとめて出力しています。

groupMapメソッドを使用することにより、mapメソッドを使用する必要がなくなったことがわかりますね。

出力は以下のようになります。

1 -> John, Nick, Daisy 2 -> Chris, Rod, Andy 3 -> Mary, Bob, Watson

生徒を学年ごとにコンマ区切りで出力することができています。

また、こちらも出力結果の各行は順不同であることに注意してください。

groupMapReduceメソッドを使用して、リストの要素を特定の基準で分類してそれぞれ処理し、最後にグループごとにまとめる

リストの要素を特定の基準で分類してそれぞれ処理し、最後にグループごとにまとめるには、groupMapReduceメソッドを使用します。

def groupMapReduce[K, B](key: (A) => K)(f: (A) => B) (reduce: (B, B) => B): immutable.Map[K, B]

引数として key: (A) => Kf: (A) => Breduce: (B, B) => B の3つを受け取ります。

第一引数は groupBy と同様に、リストの要素の何を元にして分類するかを示してあげます。

第二引数では groupMap メソッドと同様に、分類後のグループのそれぞれの要素をどう処理するかを示してあげます。

第三引数では reduce メソッドと同様に、分類後のグループのそれぞれの要素をどうひとまとめにするかを示してあげます。

戻り値として immutable.Map[K, B] を返します。 これは分類した基準値をキー(K)とし、当てはまる要素をそれぞれ処理しまとめた結果を値(B)とするマップです。

サンプルコードはこちらです。

for { tuple <- students.groupMapReduce(_.grade)(_.name)(_ + ", " + _) } println(tuple)

このコードでも同様に、生徒を学年によって分類し、名前を文字列としてまとめて出力しています。
当初のコードでは mkString のために一行消費していましたが、その代わりにこのサンプルでは名前どうしを + で結合しています。

groupしてmapしてreduceする、という一連の処理が3つの括弧としてそのままの順番でポンポンポンと並んでいますね。
このことによって、これらの処理が一体何をしているかを捉えやすいというのがこのメソッドの特長です。

出力は以下のようになります。

1 -> John, Nick, Daisy 2 -> Chris, Rod, Andy 3 -> Mary, Bob, Watson

こちらも生徒を学年ごとにコンマ区切りで出力することができていますね。

groupMapReduceメソッドのサンプルコードでは printlntuple を渡していたことにお気付きの方もいると思います。
それでも当初の例と同様に出力できている理由について説明します。

groupMapReduceメソッドの戻り値はimmutable.Map[K, B]です。
したがってこれをfor式で処理すると、それぞれの要素は Tuple2 となります。
Tuple2toStringで文字列に変換すると "key -> value"のように出力されるため、その結果、当初の例と同様に出力できたというわけです。

さて、ここでも、各行の順番が入れ替わる可能性があることには注意してください。

これらのメソッドはscala.collection.IterableOpsに定義されている

groupBygroupMapgroupMapReduceメソッドは、それぞれ scala.collection.IterableOps に定義されています。

Scala Standard Library:IterableOps#groupBy
Scala Standard Library:IterableOps#groupMap
Scala Standard Library:IterableOps#groupMapReduce

したがってこれらのメソッドは、 Listだけでなく scala.collection.mutable.ListBuffer や、scala.collection.Mapscala.collection.Set など、 IterableOps を継承しているクラスにおいても共通に利用することができます。

これは全くの余談ですが、 groupBy メソッドは、Scala 2.12 以前には scala.collection.GenTraversableLike クラスに定義されていました。
それまでは似たような名前のトレイトが何枚も重なっていたのですが、やはり「わかりにくい」という話になり、2.13において整理されました。
その結果として、groupByメソッドは IterableOps に定義されることになったというわけです。

Scala Standard Library 2.12.12:GenTraversableLike

まとめ

Listなどのコレクションクラスにおいて、要素を特定の基準で分類するにはgroupByメソッドを使用します。

また、要素を特定の基準で分類してそれぞれ処理するにはgroupMapメソッドを使用します。

また、要素を特定の基準で分類してそれぞれ処理し、最後にグループごとにまとめるにはgroupMapReduceメソッドを使用します。

サイト内検索