Rubyだとこんな風に書く、ループカウンタ付きのループ。
['a', 'b', 'c'].each_with_index { | c, idx |
puts "#{c}, #{idx}"
}
# 結果
# a, 0
# b, 1
# c, 2
こうした処理をScalaでやる場合はどうすればいいのか。
とりあえず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は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を生成するので、単純に数値をインクリメントしていく処理よりは若干遅くなるが、パフォーマンスを気にするケースでなければ十分に現実的な処理速度だと思われる。