S-JIS[2011-04-30] 変更履歴

Scala 検索実験

Scalaコレクション内の値を1つ検索する方法を考えてみる。


単純な検索

コレクションSeq等)に条件を満たす値が入っているかどうかを調べるメソッドはいくつか提供されている。

val seq = Seq(1,2,3,4,5,6,7,8,9,10)
seq.メソッド
方法 備考 関連
find
scala> seq.find(_%4 == 0)
res1: Option[Int] = Some(4)
最初に見つかった値をSomeに入れて返す。
見つからなかった場合はNoneが返る。
条件を満たす要素全てを取得したい場合はfilter
findIndexOf
scala> seq.findIndexOf(_%4 == 0)
res2: Int = 3
最初に見つかった位置を返す。
見つからなかった場合は-1が返る。
値そのもので比較したい場合はindexOf
逆順探索であるfindLastIndexOfは非推奨であり、lastIndexWhereを使う。
また、findIndexOfも内部ではindexWhereを呼んでいるので、indexWhereを使う方が良さそう。
indexWhere
scala> seq.indexWhere(_%4 == 0)
res4: Int = 3
exists
scala> seq.exists(_%4 == 0)
res8: Boolean = true
条件を満たす要素があればtrue、
無ければfalseを返す。
値そのものが存在しているかどうか(含まれているかどうか)を調べるにはcontains

変換して判定し、それを取得

コレクションSeq等)内の要素を変換して、その変換結果が条件を満たす、最初の変換結果を受け取りたい場合はどうすればよいか?


単純に考えれば、変換はmapメソッドを使うので、それとfindを組み合わせればいい。

case class C(n: Int) {
  def p = { n%4 == 0 }
}

val seq = Seq(1,2,3,4,5,6,7,8,9,10)
scala> seq.map(new C(_)).find(_.p)
res16: Option[C] = Some(C(4))

しかしこの方法だと、全ての要素に対してCのインスタンスを生成することになってしまう。
変換が軽い処理ならそれでもいいかもしれないが、重い処理では無駄が大きい。


findやexistsの様に、見つかったら途中で中断する方法を流用してみるのはどうだろう。こいつらどうやって実装しているのかな?
と思ってScala2.8.1のソースを見てみた。

TraversableLike#find
def find(p: A => Boolean): Option[A] = {
  var result: Option[A] = None
  breakable {
    for (x <- this)
      if (p(x)) { result = Some(x); break }
  }
  result
}
breakableとbreak使ってるよ!
(実態が例外発生とcatchなので、好みじゃない)
SeqLike#indexWhere
def indexWhere(p: A => Boolean, from: Int): Int = {
  var i = from
  var it = iterator.drop(from)
  while (it.hasNext) {
    if (p(it.next())) return i
    else i += 1
  }

  -1
}
途中でreturnしてるよ!
(Scalaではあまりreturnを使わない)
Iterator#find
def find(p: A => Boolean): Option[A] = {
  var res: Option[A] = None
  while (res.isEmpty && hasNext) {
    val e = next()
    if (p(e)) res = Some(e)
  }
  res
}
whileループの判定に
見つかったかどうかの条件を入れている。

ライブラリーの中は結構てんでんばらばらだし、varとかループとか剥き出しだな(苦笑)
まぁそれが隠蔽できていればいいのか。

という訳でこれを参考にすると、

implicit def seqMapFind[A](seq: Seq[A]) = new {
  def mapFind[B](f: A=>B)(p: B=>Boolean): Option[B] = {
    var res: Option[B] = None
    val i = seq.iterator
    while (res.isEmpty && i.hasNext) {
      val e = f(i.next())
      if (p(e)) res = Some(e)
    }
    res
  }
}
scala> seq.mapFind(new C(_))(_.p)
res20: Option[C] = Some(C(4))

それから、例によって再帰関数を試してみる。

implicit def seqMapFind[A](seq: Seq[A]) = new {
  def mapFind[B](f: A=>B)(p: B=>Boolean): Option[B] = {

    @scala.annotation.tailrec
    def loop(s: Seq[A]): Option[B] = {
      if (s.isEmpty) None
      else {
        val e = f(s.head)
        if (p(e)) Some(e) else loop(s.tail)
      }
    }

    loop(seq)
  }
}
scala> seq.mapFind(new C(_))(_.p)
res26: Option[C] = Some(C(4))

再帰関数を作るのに慣れてくると、これも分かりやすいかもしれないって気がしてきた…(笑)


そこまでして関数を作るのも面倒なので、出来合いのメソッドで何とかする方法も考えてみる。

scala> var res: Option[C] = None
res: Option[C] = None

scala> seq.exists{ e=> val a = new C(e); if(a.p){ res = Some(a); true }else false }
res29: Boolean = true

scala> res
res30: Option[C] = Some(C(4))

うへ、出来たけど、なんか酷い(爆)
existsを呼び出して副作用が発生しているというのが最悪だよね。


ふと、最初のパターンIteratorで試してみた。

分かりやすいように、呼び出し時のデバッグ出力を入れてみる。

case class C(n: Int) {
  println("C:" + n)

  def p = { println("p:" + n); n%4 == 0 }
}
最初のパターン Iteratorパターン
seq.map(new C(_)).find(_.p)
seq.iterator.map(new C(_)).find(_.p)
C:1
C:2
C:3
C:4
C:5
C:6
C:7
C:8
C:9
C:10
p:1
p:2
p:3
p:4
res35: Option[C] = Some(C(4))
C:1
p:1
C:2
p:2
C:3
p:3
C:4
p:4
res36: Option[C] = Some(C(4))

おおっ、これは!
単なるSeqのmap・findだと、予想通り最初に全要素に対して変換をかけてから条件判定を行っているが、
Iteratorにすると、判定ごとに変換が実行されている。

今回の要件では、これが一番シンプルな解答かな。


Scala実験へ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま