2021-4-12 追記
実際にはコレクションに対してはlengthよりsizeを使うことのほうが多いので、タイトルも 「sizeよりsizeIsを使う」 に書き換えたほうがいいかもしれませんが、lengthメソッドの内部実装を取り上げたかったのでこのままにしておきます。
はじめに
ScalaのListは、シンプルな単方向リスト(Singly Linked List、片方向リストとも)です。
headとtailを定数時間で行えることから、不変性とパフォーマンスを両立させるために昔からよく使われてきました(参考: 具象不変コレクションクラス#リスト)。
そんな便利なListですが、以下のようなケースでは少し困ったことになります。
// 1〜100万までの連番のリストを生成
val xs = List(1 to 1_000_000: _*)
if (xs.length > 10) {
println("要素数が10を超えています。")
}
上記のケースの何が問題なのでしょうか?
Listの要素数の求め方
実は、ScalaのListは自身の要素数を保持していません。
そのため上記のようにlengthメソッド(フィールドではなくメソッドです)を使用した場合、ループを通じて自身の要素数を1から数え始めるという挙動になります。1
これは実際にソースコードを見たほうが早いかもしれません。
override final def length: Int = {
var these = this
var len = 0
while (!these.isEmpty) {
len += 1
these = these.tail
}
len
}
見ての通り、自身が空になるまでインクリメントとtailを続けることで、最終的な要素数を算出しています。
「10より大きいかどうか」を知りたいだけなのに
以上の前提を踏まえた上で、もう一度最初の例を見てみます。
// 1〜100万までの連番のリストを生成
val xs = List(1 to 1_000_000: _*)
if (xs.length > 10) {
println("要素数が10を超えています。")
}
ここでlengthメソッドは、Listの要素数が10を超えているかどうかを調べるためだけに使われています。
確かにこれでも望む答えを得ることはできますが、先程のlengthの定義と照らし合わせると、「要素数が10を超えているかどうか」を調べるために100万回のループが行われることになります。
なおこれはlengthでなくsizeメソッドで要素数を調べた場合も同様です。2
また下記のようなケースも考えられます。
if (xs.length <= 10) {
println("要素数が10以下です。")
}
if (xs.length == 10) {
println("要素数は10です。")
}
上記のケースは全て、最高でも11までカウントすれば事足ります。
要素数が11以上ならその時点で> 10はtrueとわかりますし、 <= 10や== 10はfalseになります。
残りの999,989回分のカウントは明らかに無駄なので、「要素数が11以上」とわかった段階で処理を打ち切りたいところです。
これをしてくれるのが、Scala2.13で追加されたlengthIsメソッドです。
lengthIsメソッド
lengthIsメソッド3 は、下記のように使用します。
val xs = List(1 to 1_000_000: _*)
if (xs.lengthIs > 10) {
println("要素数が10を超えています。")
}
見た目はlengthを使用した場合とほとんど変わりませんが、内部的には必要に応じて処理を打ち切っているので、より効率的となっています。
>に限らず、>= < <= == !=の全てで使用可能です。
Scala2.13以降で上記のような比較を行いたい場合、積極的にlengthIsメソッドを使用していくのがいいでしょう。
なお、lengthIsとは別にsizeIsというメソッドも用意されていますが、lengthとsizeが同じものであるのと同様に、これらも全く同じ挙動となります。
if (xs.sizeIs > 10) { // lengthIsと全く同じ
println("要素数が10を超えています。")
}
おまけ1 - IndexedSeq系列のクラス(とArray)の場合
lengthIsメソッドはSeqのメソッドなので、Listや別のLinerSeq系列のクラスに限らず、RangeやVectorのようなIndexedSeq系列のクラスでも、またはArrayでも使用可能です。
val xs = 1 to 1_000_000
val ys = Vector(xs: _*)
val zs = Array(xs: _*)
if (xs.lengthIs > 10 && ys.lengthIs > 10 && zs.lengthIs > 10) {
...
}
しかしこれらのクラスはそもそもlengthの演算自体が不要または高速なクラスです。
なのでこれらのクラスに対してListのようにカウントのためのループを一つずつ回してしまうと、(途中までとはいえ)逆にlengthで比較するより遅くなってしまいます。
そのためlengthIsメソッドは(内部的に)これらのクラスではlengthを呼び出しており、ループによるカウントは行いません。4
ループによる性能劣化の懸念がなくなるので、利用者はSeqの種類がLinerSeqなのかIndexedSeqなのかなどを気にせず、比較したいときは気軽にlengthIsメソッドを使えるようになっています。
おまけ2 - Scala2.12以前の場合
lengthIsメソッドはScala2.13から追加されたメソッドなので、2.12以前では使えません。
2.12以前で上記のような比較を行いたい場合、lengthCompareメソッドを使うことで、lengthIsメソッドと同じく無駄のない比較が可能です。
if (xs.lengthCompare(10) > 0) {
println("要素数が10を超えています。")
}
lengthCompareメソッドはよくあるcompareメソッドと同様に、-1,0,1のいずれかを返すことで大小を表します。5
2.13以降であればlengthIsメソッドの方が直感的ですし、積極的に使う理由はないと思われますが、2.12以前を扱う際は覚えておいて損はないと思います。
最後までお読み頂きありがとうございました。 質問や不備についてはコメント欄か[Twitter](https://twitter.com/ka2_kamaboko)までお願いします。