each_with_index

Rubyだとこんな風に書く、ループカウンタ付きのループ。

['a', 'b', 'c'].each_with_index { | c, idx |
  puts "#{c}, #{idx}"
}
# 結果
# a, 0
# b, 1
# c, 2

こうした処理をScalaでやる場合はどうすればいいのか。

@CretedDate 2012/01/24
@Versions Scala2.9.1

zipWithIndex

とりあえずzipWithIndexを使えばできるらしい。

zipWithIndexは、List[T] を List[(T, Int)]に変換してくれる(Intの部分にインデックスが入る)。

val list = List('a', 'b', 'c')
list.zipWithIndex

// 結果
// List((a,0), (b,1), (c,2))

これをループで回せば、Rubyのeach_with_indexみたいな挙動になる。

list.zipWithIndex foreach { case (c, i) => println(c, i) }

// 結果
// (a,0)
// (b,1)
// (c,2)

コード的にはforで書いた方が若干短いか。

for( (c, i) <- list.zipWithIndex ) println(c, i)

zipWithIndexのパフォーマンス

zipWithIndexはscala.collection.IterableLikeに実装されている。処理内容的には以下のような感じで、ループで回しながらBufferに要素と添字を詰め直している。

// scala.collection.IterableLike
def zipWithIndex[A1 >: A, That](implicit bf: CanBuildFrom[Repr, (A1, Int), That]): That = {
  val b = bf(repr)
  var i = 0
  for (x <- this) {
    b += ((x, i))
    i +=1 
  }
  b.result
}

この内容だと要素数の大きいCollectionを扱った場合は、パフォーマンスが少し心配。

これに対して、IterableLikeではなくscala.collection.Iteratorに実装されているzipWithIndexは、回るたびに番号をインクリメントしていくようなコードになっている。

// scala.collection.IteratorのzipWithIndexのコード
def zipWithIndex = new Iterator[(A, Int)] {
  var idx = 0
  def hasNext = self.hasNext
  def next = {
    val ret = (self.next, idx)
    idx += 1
    ret
  }
}

単純にループを回すだけなら、以下のようにItaratorのzipWithIndexを呼び出す記述にした方が良さそう。

// foreach
list.iterator.zipWithIndex foreach { case (c, i) => println(c, i) }

// for
for( (c, i) <- list.iterator.zipWithIndex) println(c, i)

速度測定

本当にIterableLikeとIteratorで速度に差が出るのか確認してみたところ、要素数が1千万の場合で処理時間に10倍以上の差が出た。

下記は毎回JVMを立ち上げ直す形で各処理を5回ずつ実行した際の実行時間のメモ。

まずはIterableLinkeのzipWithIndex。

(0 to 10000000).zipWithIndex foreach { case (c, i) => () }

// 実行時間(5回)
// 8195, 8200, 8130, 8098, 8128 (平均8150.2ミリ秒) 

次にIteratorのzipWithIndex。

(0 to 10000000).iterator.zipWithIndex foreach { case (c, i) => () }

// 実行時間(5回)
// 503, 474, 456, 462, 456(平均470.2ミリ秒)

添字付きで回すだけならfoldLeftでもいける気がしたのでやってみたら速かった。

( 0 /: ( 0 to 10000000 ) ) { ( idx, i ) => idx + 1 }

// 実行時間(5回)
// 241, 247, 267, 245, 257(平均251.4ミリ秒)

単純に最速を目指すならこう書けば良いか。

def loop[T](ite: Iterator[T], idx: Int): Unit = {
  ite.next
  if (ite.hasNext) loop(ite, idx + 1)
}
loop((0 to 10000000).iterator, 0)

// 実行時間(5回)
// 110, 101, 113, 106, 108(107.6ミリ秒)

計測値を図にするとこんな感じになった。

Iterableに対してzipWithIndexするとやはりかなり遅い。扱う要素の内容によっては避けた方が良さそう。

IteratorのzipWithIndexも、毎回添字の入ったTupleを生成するので、単純に数値をインクリメントしていく処理よりは若干遅くなるが、パフォーマンスを気にするケースでなければ十分に現実的な処理速度だと思われる。