マイナーアップデートのしやすさ
可変性を使用する理由の1つは、進行中のプロセスを追跡している場合です。たとえば、大きなドキュメントを編集していて、テキストのさまざまな要素、編集履歴、カーソル位置などを追跡するための複雑なクラスのセットがあるとします。ここで、ユーザーがテキストの別の部分をクリックしたとします。EditState
多くのフィールドをコピーして、フィールドをコピーせずに、ドキュメントオブジェクトを再作成しますか?EditState
newViewBounds
とdocumentCursorPosition
?で再作成します。または、1つの場所で可変変数を変更しますか? スレッドセーフが問題にならない限り、すべてをコピーするよりも、変数を1つか2つ更新する方がはるかに簡単で、エラーが発生しにくくなります。スレッドセーフが問題の場合、同時アクセスから保護することは、不変のアプローチを使用して古い要求を処理するよりも多くの作業になる可能性があります。
計算効率
可変性を使用するもう1つの理由は、速度です。オブジェクトの作成は安価ですが、単純なメソッド呼び出しは安価であり、プリミティブ型の操作はさらに安価です。
たとえば、マップがあり、値と値の2乗を合計したいとします。
val xs = List.range(1,10000).map(x => x.toString -> x).toMap
val sum = xs.values.sum
val sumsq = xs.values.map(x => x*x).sum
たまにこれをやれば大したことではありません。しかし、何が起こっているかに注意を払うと、すべてのリスト要素について、最初にそれを再作成し(値)、次にそれを合計し(ボックス化)、次にそれを再作成し(値)、次にボックス化して正方形の形で再作成します(マップ) 、次にそれを合計します。これは、アイテムごとに2つの加算と1つの乗算を行うために、少なくとも6つのオブジェクトの作成と5つの完全なトラバーサルです。 信じられないほど非効率的です。
フォールドを使用して、複数の再帰を回避し、マップを1回だけ通過することで、より良い結果を得ることができます。
val (sum,sumsq) = ((0,0) /: xs){ case ((sum,sumsq),(_,v)) => (sum + v, sumsq + v*v) }
そして、これははるかに優れており、私のマシンのパフォーマンスは約15倍向上しています。ただし、反復ごとに3つのオブジェクトが作成されます。代わりにあなたが
case class SSq(var sum: Int = 0, var sumsq: Int = 0) {
def +=(i: Int) { sum += i; sumsq += i*i }
}
val ssq = SSq()
xs.foreach(x => ssq += x._2)
あなたはボクシングを減らしたので、あなたは再び約2倍速くなります。データが配列にあり、whileループを使用している場合は、すべてのオブジェクトの作成とボクシングを回避し、さらに20倍高速化できます。
そうは言っても、配列に再帰関数を選択することもできます。
val ar = Array.range(0,10000)
def suma(xs: Array[Int], start: Int = 0, sum: Int = 0, sumsq: Int = 0): (Int,Int) = {
if (start >= xs.length) (sum, sumsq)
else suma(xs, start+1, sum+xs(start), sumsq + xs(start)*xs(start))
}
このように書くと、可変SSqと同じくらい高速になります。しかし、代わりにこれを行う場合:
def sumb(xs: Array[Int], start: Int = 0, ssq: (Int,Int) = (0,0)): (Int,Int) = {
if (start >= xs.length) ssq
else sumb(xs, start+1, (ssq._1+xs(start), ssq._2 + xs(start)*xs(start)))
}
各ステップでオブジェクトを作成する必要があるため、再び10倍遅くなりました。
つまり、重要なのは、メソッドへの独立した引数として更新構造を便利に実行できない場合にのみ、不変性があることが重要であるということです。それが機能する複雑さを超えれば、可変性は大きな勝利になる可能性があります。
累積オブジェクトの作成
潜在的に障害のあるデータからのフィールドを使用して複雑なオブジェクトを構築する必要がある場合はn
、次のようなビルダーパターンを使用できます。
abstract class Built {
def x: Int
def y: String
def z: Boolean
}
private class Building extends Built {
var x: Int = _
var y: String = _
var z: Boolean = _
}
def buildFromWhatever: Option[Built] = {
val b = new Building
b.x = something
if (thereIsAProblem) return None
b.y = somethingElse
// check
...
Some(b)
}
これは、可変データでのみ機能します。もちろん、他のオプションもあります。
class Built(val x: Int = 0, val y: String = "", val z: Boolean = false) {}
def buildFromWhatever: Option[Built] = {
val b0 = new Built
val b1 = b0.copy(x = something)
if (thereIsAProblem) return None
...
Some(b)
}
これは多くの点でさらにクリーンですが、変更を加えるたびにオブジェクトを1回コピーする必要があり、非常に遅くなる可能性があります。そして、これらはどちらも特に防弾ではありません。そのためにあなたはおそらく欲しいでしょう
class Built(val x: Int, val y: String, val z: Boolean) {}
class Building(
val x: Option[Int] = None, val y: Option[String] = None, val z: Option[Boolean] = None
) {
def build: Option[Built] = for (x0 <- x; y0 <- y; z0 <- z) yield new Built(x,y,z)
}
def buildFromWhatever: Option[Build] = {
val b0 = new Building
val b1 = b0.copy(x = somethingIfNotProblem)
...
bN.build
}
しかし、繰り返しになりますが、多くのオーバーヘッドがあります。