倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Scala の Seq に定義されているメソッドを試す (9) ~タプル関連~

Scala の Seq に定義されているメソッドを試すシリーズ(目次)。 今回は Seq のタプルやタプルの Seq を返すメソッドを見ていきます。 Map はタプルの Seq なので groupBy メソッドもここで扱います。

今回扱うメソッド

def splitAt(n: Int): (Seq[A], Seq[A])
def span(p: (A) ⇒ Boolean): (Seq[A], Seq[A])
def partition(p: (A) ⇒ Boolean): (Seq[A], Seq[A])

def groupBy[K](f: (A) ⇒ K): Map[K, Seq[A]]

def zip[B](that: GenIterable[B]): Seq[(A, B)]
def zipAll[B](that: collection.Iterable[B], thisElem: A, thatElem: B): Seq[(A, B)]
def zipWithIndex: Seq[(A, Int)]

def unzip[A1, A2](implicit asPair: (A) ⇒ (A1, A2)): (Seq[A1], Seq[A2])
def unzip3[A1, A2, A3](implicit asTriple: (A) ⇒ (A1, A2, A3)): (Seq[A1], Seq[A2], Seq[A3])

サンプルコード

splitAt メソッド
splitAt メソッドは、指定した位置で Seq を2つに分割します。 take と drop で得られる部分 Seq をタプルにしたものと同じものが返されます(個別に呼ぶよりパフォーマンスはいいんでしょうけど)。

  val strSeq = Seq("a", "b", "c", "d", "e")

  // split メソッド
  assert( strSeq.splitAt(3) == (Seq("a", "b", "c"), Seq("d", "e")) )

  // take と drop で返される Seq をタプルにしたものと同じ
  assert( strSeq.splitAt(3) == (strSeq.take(3), strSeq.drop(3)) )

span メソッド
span メソッドは、要素に対する述語関数(Boolean 値を返す関数、条件式)を引数にとり、先頭からその条件を満たしている要素を集めて結果のタプルの第1要素に、それ以降の要素を集めて結果のタプルの第2要素にします。 takeWhile と dropWhile で返される Seq をタプルにして返す、と言った方が分かりやすいですかね。

  val intSeq = Seq(0, 1, 2, 3, 0, 1, 2, 3)

  // span メソッド
  assert( intSeq.span(_ < 3) == (Seq(0, 1, 2), Seq(3, 0, 1, 2, 3)) )

  // takeWhile と dropWhile で返される Seq をタプルにしたものと同じ
  val f: Int => Boolean = _ < 3
  assert( intSeq.span(f) == (intSeq.takeWhile(f), intSeq.dropWhile(f)) )

条件を満たさない要素が現れると、それ以降に条件を満たす要素が現れてもすべて結果のタプルの第2要素に含まれます。

partition メソッド
partition メソッドは、span メソッドと同じように要素に対する述語関数を引数にとりますが、要素全てを走査して、条件に合う要素を返り値のタプルの第1要素に、条件に合わない要素を返り値のタプルの第2要素にします。 言い換えると、filter と filterNot が返す Seq をタプルにしたものと同じものを返します。

  val intSeq = Seq(0, 1, 2, 3, 0, 1, 2, 3)

  // partition メソッド
  assert( intSeq.partition(_ < 3) == (Seq(0, 1, 2, 0, 1, 2), Seq(3, 3)) )

  // filter と filterNot で返される Seq をタプルにしたものと同じ
  assert( intSeq.partition(f) == (intSeq.filter(f), intSeq.filterNot(f)))

groupBy メソッド
groupBy メソッドは、要素を別の型のオブジェクトに変換し、変換後のオブジェクトが同じになる要素を集めて Map にしたものを返します。 数学っぽく言うと同値類を作るようなものですね。 返される Map のキーは変換後のオブジェクト、値はそのキーに変換される元の Seq の要素を集めた Seq オブジェクトとなります。

  val intSeq = Seq(0, 1, 2, 3, 4)

  // groupBy メソッド
  assert( intSeq.groupBy(_ % 3) == // 3で割った余りでグループ分け
    Map(0 -> Seq(0, 3),  // 3で割った余りが0
        1 -> Seq(1, 4),
        2 -> Seq(2)) )

前述の partition メソッドは、述語関数によって元の Seq の要素を Boolean 値に変換していると思えば、groupBy メソッドみたいなものとも思えます。

  val intSeq = Seq(0, 1, 2, 3, 0, 1, 2, 3)
  val f: Int => Boolean = _ < 3

  // groupBy メソッドと partition メソッド
  val parts = intSeq.partition(f)
  assert( intSeq.groupBy(f) == 
    Map(true  -> parts._1,  // 真偽値をキーとした Map
        false -> parts._2) )

zip メソッド
zip メソッドは、2つの Seq を先頭から順々にタプルにして Seq として返します。 返される Seq の n 番目の要素は、もとの2つの Seq の n 番目の要素をタプルにしたものになります。

  val strSeq = Seq("a", "b", "c", "d", "e")
  val intSeq0 = Seq(0, 1, 2, 3, 4)

  // zip メソッド
  assert( (strSeq zip intSeq0) ==
    Seq(
      ("a", 0),
      ("b", 1),
      ("c", 2),
      ("d", 3),
      ("e", 4)) )

2つの Seq の長さが異なる場合は、短い方に合わせられます。

  val strSeq = Seq("a", "b", "c", "d", "e")
  val intSeq1 = Seq(0, 1, 2)

  // Seq の長さが違う場合
  assert( (strSeq zip intSeq1) ==
    Seq(
      ("a", 0),
      ("b", 1),
      ("c", 2)) )

zip メソッドがやっていることは tranpose メソッドに似てるので、実際に transpose で書いてみると以下のようになります。

  val strSeq = Seq("a", "b", "c", "d", "e")
  val intSeq0 = Seq(0, 1, 2, 3, 4)

  // transpose 使って書いてみた
  assert( (strSeq zip intSeq0) ==
    Seq(strSeq, intSeq0).transpose.map(seq => (seq(0), seq(1))) )

Seq をタプルに返る map メソッド呼び出しがありますが、やってることは transpose のような組み替えですね。

zipAll メソッド
zip メソッドは2つの Seq の長さが異なる場合には短い方に合わせるように長い方の要素を切り捨てましたが、zipAll メソッドは長い方に合わせ、足りない分は引数に指定したオブジェクトで補います。

  val strSeq = Seq("a", "b", "c", "d", "e")
  val intSeq = Seq(0, 1, 2)

  // zipAll メソッド
  val result = strSeq.zipAll(intSeq, "null", -1)
  assert( result ==
    Seq(
      ("a", 0),
      ("b", 1),
      ("c", 2),
      ("d", -1),  // intSeq の長さが足りないので-1で補う
      ("e", -1)) )

今の場合、strSeq の方が短ければタプルの第1要素が "null" によって補われます。

zipWithIndex メソッド
zipWithIndex メソッドは、Seq のインデックスとの zip 結果を返します。

seq.zip(seq.indices)

インデックスの Int 値は0から始まり、タプルの第2要素となります。

  val strSeq = Seq("a", "b", "c", "d", "e")

  // zipWithIndex メソッド
  assert( strSeq.zipWithIndex ==
    Seq(
      ("a", 0),
      ("b", 1),
      ("c", 2),
      ("d", 3),
      ("e", 4)) )

使い方は以下のようになります。

  strSeq.zipWithIndex.map(x => s"${x._2}番目は${x._1}").foreach(println)

実行すると以下のような結果が表示されます。

0番目はa
1番目はb
2番目はc
3番目はd
4番目はe

もしくは、case 文で部分関数を書いて以下のようにすることもできます。

  strSeq.zipWithIndex.map{
    case (s, i) => s"${i}番目は$s"
  }.foreach(println)

実行結果は上記と同じです。

unzip メソッド
unzip メソッドは、zip メソッドとは逆に、1つの Seq オブジェクトを2つの Seq のタプルに分割します。 unzip メソッドの引数には、元の Seq オブジェクトの要素をタプルに変換する関数を渡します。

  val intSeq = Seq(0, 1, 2, 3, 4)

  // unzip メソッド
  val result = intSeq.unzip(n => (n/3, n%3))  // 3で割った商と余りのタプル
  assert( result == 
    (Seq(0, 0, 0, 1, 1),
     Seq(0, 1, 2, 0, 1)) )

zip メソッドも transpose みたいなことをしてるので transpose で書いてみた。

  val intSeq = Seq(0, 1, 2, 3, 4)

  // zip メソッドを transpose で書いてみた
  val transposed = intSeq.map(n => Seq(n/3, n%3)).transpose
  assert( intSeq.unzip(n => (n/3, n%3)) == (transposed(0), transposed(1)) )

unzip3 メソッド
unzip3 メソッドは概ね unzip メソッドと同じですが、2要素のタプルではなく3要素のタプルを扱います。

  case class Person(name: String, age: Int, sex: String)

  val persons = Seq(Person("Alice", 40, "female"),
                    Person("Bob", 30, "male"),
                    Person("Eve", 20, "female"))

  // unzip3 メソッド
  assert( persons.unzip3(p => (p.name, p.age, p.sex)) ==
    (Seq("Alice", "Bob", "Eve"),
     Seq(40, 30, 20),
     Seq("female", "male", "female")) )

unzip3 メソッドも transpose を使って書けると思いますが、unzip の場合と大して変わらないので止めておきます。

前回と次回で多重コレクションに関連するメソッドを試してみました。 少々込み入った処理をしようとしたときに、これらのメソッドを使うと意外と簡潔に書けたことがしばしばあったので、要チェックなメソッド群です。 次回は数学関連のメソッドを試していく予定。

Scalaスケーラブルプログラミング第3版

Scalaスケーラブルプログラミング第3版