15

Scalaでのプログラミング(第2版)から、p.98の下部:

Scalaプログラマーのためのバランスの取れた態度

vals、不変オブジェクト、および副作用のないメソッドを優先します。最初に彼らに手を差し伸べてください。特定のニーズと正当性がある場合は、var、可変オブジェクト、および副作用のあるメソッドを使用します。

前のページで、vals、不変オブジェクト、および副作用のないメソッドを好む理由を説明しているので、この文は完全に理にかなっています。

しかし、2番目の文:「特定のニーズと正当性がある場合は、var、可変オブジェクト、および副作用のあるメソッドを使用してください。」あまりよく説明されていません。

だから私の質問は:

副作用のある変数、可変オブジェクト、およびメソッドを使用する正当化または特定の必要性は何ですか?


追伸:誰かがそれらのそれぞれについていくつかの例を提供できれば素晴らしいと思います(説明以外に)。

4

4 に答える 4

16

多くの場合、関数型プログラミングは抽象化のレベルを高め、コードをより簡潔にし、記述と理解をより簡単/迅速にします。ただし、結果のバイトコードを命令型ソリューションのように最適化(高速)できない場合があります。

現在(Scala 2.9.1)1つの良い例は、範囲を合計することです。

(1 to 1000000).foldLeft(0)(_ + _)

対:

var x = 1
var sum = 0
while (x <= 1000000) {
  sum += x
  x += 1
}

これらをプロファイリングすると、実行速度に大きな違いが見られます。したがって、パフォーマンスが本当に正当な理由となる場合があります。

于 2012-02-01T10:48:39.480 に答える
8

マイナーアップデートのしやすさ

可変性を使用する理由の1つは、進行中のプロセスを追跡している場合です。たとえば、大きなドキュメントを編集していて、テキストのさまざまな要素、編集履歴、カーソル位置などを追跡するための複雑なクラスのセットがあるとします。ここで、ユーザーがテキストの別の部分をクリックしたとします。EditState多くのフィールドをコピーして、フィールドをコピーせずに、ドキュメントオブジェクトを再作成しますか?EditStatenewViewBoundsdocumentCursorPosition?で再作成します。または、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
}

しかし、繰り返しになりますが、多くのオーバーヘッドがあります。

于 2012-02-01T16:12:23.897 に答える
5

命令型/可変型のスタイルが動的計画法アルゴリズムに適していることがわかりました。不変性を主張すると、ほとんどの人にとってプログラミングが難しくなり、大量のメモリを使用したり、スタックをオーバーフローさせたりすることになります。一例:関数型パラダイムにおける動的計画法

于 2012-02-01T15:14:45.700 に答える
3

いくつかの例:

  1. (元々はコメント)どのプログラムも何らかの入力と出力を行わなければなりません(そうでなければ、それは役に立ちません)。しかし、定義上、入出力は副作用であり、副作用のあるメソッドを呼び出さずに実行することはできません。

  2. Scalaの主な利点の1つは、Javaライブラリを使用できることです。それらの多くは、副作用のある可変オブジェクトとメソッドに依存しています。

  3. 時々あなたはvarスコーピングのために必要です。例については、このブログ投稿を参照Temperature4してください。

  4. 並行プログラミング。アクターを使用する場合、メッセージの送受信は副作用です。スレッドを使用する場合、ロックの同期は副作用であり、ロックは変更可能です。イベント駆動型の並行性は、すべて副作用に関するものです。先物、同時コレクションなどは変更可能です。

于 2012-02-01T12:36:15.597 に答える