倭マン's BLOG

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

Scala の Seq に定義されているメソッドを試す (3) ~要素の取得~

Scala の Seq に定義されているメソッドを試すシリーズ(目次)。 今回は Seq に格納されている要素を取得するメソッドと、各要素に対して副作用のある処理を行うメソッド(まぁ、foreach メソッドなんですが)を試します。

今回扱うメソッド

まずは要素を取得するメソッド。 Java の配列のようにインデックスを指定して要素を取得するには apply() メソッドを使います。

def apply(idx: Int): A
def applyOrElse[A1 <: Int, B1 >: A](x: A1, default: (A1) ⇒ B1): B1

def head: A
def headOption: Option[A]
def tail: Seq[A]

def last: A
def lastOption: Option[A]
def init: Seq[A]

def find(p: (A) ⇒ Boolean): Option[A]
def collectFirst[B](pf: PartialFunction[A, B]): Option[B]

また、各要素を走査して副作用を伴う処理を実行するメソッドは次の2つ:

def foreach(f: (A) ⇒ Unit): Unit
def runWith[U](action: (A) ⇒ U): (Int) ⇒ Boolean

foreach() メソッドはいろいろな言語に同様のメソッドがあるので使い方は問題ないでしょう。

Seq[A] は PartialFunction[Int, A] トレイト(部分関数)を拡張していますが、上記の中で

  • apply メソッド
  • applyOrElse メソッド
  • runWith メソッド

は PartialFunction トレイトから受け継いでいるメソッドです(他にもありますが)。

サンプルコード(要素の取得)

ではサンプルコードを見ていきましょう。 前回までは Int 値を要素とする Seq オブジェクトを主に使っていましたが、今回は String を要素とする Seq オブジェクトを使います。

apply メソッド
まずは基本となる、インデックスを指定して要素を取り出す apply メソッド。 Scala では apply メソッドは () で呼び出せますね(念のため)。

  val seq = Seq("zero", "one", "two", "three", "four")

  // apply メソッド
  assert( seq(0) == "zero" )  // seq.head の方が望ましい
  assert( seq(3) == "three" )

  // 範囲外の要素にアクセスしたら例外を投げる
  try{
    seq(10)  // IndexOutOfBoundsException が投げられる
    assert(false)
  }catch{
    case ex: IndexOutOfBoundsException => assert( true )
  }

0番目の要素にアクセスする場合には、後で見る head メソッドを使った方が高速でアクセスできます(List などの LinearSeq の場合)。 指定したインデックスに要素がなかった場合は例外 (IndexOutOfBoundsException) が投げられます。

applyOrElse メソッド
applyOrElse メソッドは指定したインデックスに対応する要素があれば apply メソッドと同じようにその要素を返しますが、対応する要素がなかった場合に、apply メソッドのように例外を投げるのではなく、第2引数に指定した関数を評価した結果を返します。 第2引数に指定する関数は、引数が Int 値で返り値が要素の型とします。 まぁ、計算できるデフォルト値を設定して、対応する要素がなくても例外が投げられないようにする apply メソッドですね。

  val seq = Seq("zero", "one", "two", "three", "four")

  // applyOrElse メソッド
  assert( seq.applyOrElse(0, (i: Int) => i.toString) == "zero")
  assert( seq.applyOrElse(3, (_:Int).toString) == "three" )
  assert( seq.applyOrElse(10, Integer.toString) == "10" )

第2引数に渡す関数をいろいろな形で書いてみました(他にも書き方ありますが)。 同じ関数を使うなら変数に格納して使い回した方がいいかと思います。

head メソッド
head メソッドは先頭(インデックスが0)の要素を取得します。 List などの LinearSeq を拡張する Seq では、apply メソッドに0を指定して要素を取得するより高速です。

  val seq = Seq("zero", "one", "two", "three", "four")

  // head メソッド
  assert( seq.head == "zero")
  assert( seq.head == seq(0))  // インデックス0でも同じ要素を取得可

  try{
    Seq.empty.head
    // 空の Seq に対しては例外を投げる
    assert(false)
  }catch{
    case ex: NoSuchElementException => assert( true )
  }

空の Seq に対して呼び出すと例外 (NoSuchElementException) を投げます。

headOption メソッド
headOption メソッドは head メソッドのように先頭の要素を取得するメソッドですが、空の Seq に対して呼び出しても例外が投げられないように Option で包んで返します。 したがって、要素型が A なら、headOption メソッドの返り値の型は Option[A] となります。

  val seq = Seq("zero", "one", "two", "three", "four")

  // headOption メソッド
  assert( seq.headOption == Some("zero") )
  assert( Seq.empty.headOption == None )

  // match などといっしょに使うのがいいかな?
  seq.headOption match {
    case Some(s) => println("[headOption] " + s)
    case None    => println("[headOption] error!")
  }
    // 「[headOption] zero」と表示される
    // seq の代わりに空の Seq に対して呼び出すと「[headOption] error!」と表示される

tail メソッド
tail メソッドは、元の Seq オブジェクトから head 要素を除いた Seq オブジェクトを返します。 head と tail で元の Seq を要素の過不足無く分割します。 このメソッドも LinearSeq では高速です。

  val seq = Seq("zero", "one", "two", "three", "four")

  // tail メソッド
  assert( seq.tail == Seq("one", "two", "three", "four") )
  assert( (seq.head +: seq.tail) == seq)  // 「+:」まだやってないけど。
  assert( Seq("zero").tail == Nil )  // 要素が1つの場合は tail は空 Seq

  try{
    Seq.empty.tail
    // 空 Seq に対しては例外を投げる
    assert(false)
  }catch{
    case ex: UnsupportedOperationException => assert(true)
  }

1つの要素しか持たない Seq に対して tail メソッドを呼び出すと空の Seq が返され、空の Seq に対して tail メソッドを呼び出すと例外が投げられます。

last メソッド
last メソッドは head メソッドとは逆に、Seq の最後の要素を返します。 List などの LinearSeq を拡張する Seq に対して呼び出すと、要素数が多ければパフォーマンスが劣化するので注意。 使い方は head メソッドと同じです。

  val seq = Seq("zero", "one", "two", "three", "four")

  // last メソッド
  assert( seq.last == "four" )
  assert( seq.last == seq(seq.length-1) )

  try{
    Seq.empty.last
    // 空の Seq に対して呼び出すと例外を投げる
    assert(false)
  }catch{
    case ex: NoSuchElementException => assert( true )
  }

空の Seq に対して last メソッドを呼び出すと、head メソッドの場合と同じように例外を投げます。

lastOption メソッド
lastOption メソッドは、最後の要素を取得する際に、最後の要素がなくても例外が投げられないように Option で包むメソッドです。 head メソッドに対する headOption メソッドと同じです。 使い方は headOption と同じ。

  val seq = Seq("zero", "one", "two", "three", "four")

  // lastOption メソッド
  assert( seq.lastOption == Some("four") )
  assert( emp.lastOption == None )

  // match といっしょに使う
  seq.lastOption match {
    case Some(s) => println("[lastOption] " + s)
    case None    => println("[lastOption] error!")
  }
    // 「[lastOption] four」と表示される

init メソッド
init メソッドは、元の Seq オブジェクトから last 要素を除いた Seq オブジェクトを返します。 init と last で元の Seq を過不足なく分割します。 init メソッドも LinearSeq に対してはパフォーマンスを劣化させます。

  val seq = Seq("zero", "one", "two", "three", "four")

  // init メソッド
  assert( seq.init == Seq("zero", "one", "two", "three") )
  assert( (seq.init :+ seq.last) == seq )  // 「:+」もまだやってない
  assert( Seq("zero").init == Nil )  // 要素が1つの Seq に足しては空の Seq を返す

  try{
    Seq.empty.init
    // 空の Seq に対して呼び出すと例外を投げる
    assert(false)
  }catch{
    case ex: UnsupportedOperationException => assert(true)
  }

find メソッド
find メソッドは、述語関数(条件)を指定して、それに適合する最初の要素を返します。 ただし、適合する要素がない場合にも例外が投げられないように、Option で包みます。

  val seq = Seq("zero", "one", "two", "three", "four")

  // find メソッド
  assert( seq.find(_.length == 3) == Some("one") )  // 文字列長が3の要素
  assert( seq.find(_.length == 2) == None )

  // match といっしょに使ってみた
  seq.find(_.length == 3) match {
    case Some(s) => println("[find] " + s)
    case None    => println("[find] error!")
  }
    // 「[find] one」と表示される

collectFirst メソッド
collectFirst メソッドは PartialFunction オブジェクトを引数にとり、Seq オブジェクトの要素の中で最初にその PartialFunction の定義域に含まれている(isDefinedAt メソッドが true を返す)要素に対して部分関数を適用して、その結果を返します。 ただし、定義域に含まれている要素がない場合にも例外が投げられないように Option で包みます。 大雑把には、find メソッドの結果(None でなければ)の中身に変換を施して返すようなメソッドです。

  val seq = Seq("zero", "one", "two", "three", "four")

  // collectFirst メソッド
  val result = seq.collectFirst{
    case s if s.startsWith("t") =>  // "t" で始まる文字列がこの部分関数の定義域
      "T" + s.substring(1)          // 最初の "t" を "T" に変えて返す
  }
  assert( result == Some("Two") )

  // match といっしょに使ってみた
  seq.collectFirst{
    case s if s.startsWith("t") => "T" + s.substring(1)
  } match {
    case Some(s) => println("[collectFirst] " + s)
    case None    => println("[collectFirst] error!")
  }
    // 「[collectFirst] Two」と表示される

なんかサンプルコードがいろいろぎこちないですが、まぁあまり気にしない方向で。

サンプルコード(副作用を伴う処理)

次は副作用を伴う処理を行うメソッド。 副作用を伴うと言っても元の Seq を変更するわけではなく、返り値が Unit の関数とともに使うという意味です。

foreach メソッド
foreach メソッドは、要素に対する副作用を伴う関数を引数にとり、各要素にその処理を施します。 Seq に含まれる要素全てに(副作用を伴う)同じ処理を行いたいときに使います。

  val seq = Seq("zero", "one", "two", "three", "four")

  // foreach メソッド
  seq.foreach { s =>
    println("[foreach] " + s)
  }

runWith メソッド(PartialFunction より)
runWith メソッドは PartialFunction トレイトに定義されているメソッドです。 副作用を伴う関数(返り値が Unit の関数)を引数にとるので foreach メソッドといっしょに書いてますが、あまり要素の取得という感じのメソッドではありません。 どの程度使われるメソッドなのか分かりませんが、説明が面倒なのでサンプルコードのみで。

  val classmates = Seq("Alice", "Bob", "Eve")

  // runWith メソッド
  val greet: Int => Boolean =  // greet の型指定は別になくても OK
    classmates.runWith{ s =>
      println(s"Hello $s!")
    }

  greet(0)  // 「Hello Alice!」と表示(true を返す)
  greet(2)  // 「Hello Eve!」と表示(true を返す)
  greet(3)  // 何も表示しない(false を返す)

いつかこれが使える局面に出会うんでしょうか。

今回は Seq に格納されている要素を取得するメソッドを主に試してきました。 次回はインデックス関連のメソッドを試す予定。

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

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