簡體   English   中英

當命令式的風格更合適?

[英]When imperative style fits better?

來自Scala編程(第二版) ,第98頁的底部:

對Scala程序員的平衡態度

首選vals,不可變對象和沒有副作用的方法。 首先到達他們。 當您有特定的需求和理由時,請使用變量,可變對象和帶副作用的方法。

在前幾頁解釋了為什么更喜歡val,不可變對象和沒有副作用的方法,所以這句話很有意義。

但第二句話:“當你有特殊的需要和理由時,使用變量,可變對象和副作用的方法。” 沒有這么好解釋。

所以我的問題是:

什么是使用變量,可變對象和具有副作用的方法的理由或特定需要?


Ps:如果有人可以為每個人提供一些例子(除了解釋),那將是很棒的。

在許多情況下,函數式編程可以提高抽象級別,從而使您的代碼更簡潔,更容易/更快地編寫和理解。 但是有些情況下,生成的字節碼不能像命令式解決方案那樣優化(快速)。

目前(Scala 2.9.1)一個很好的例子是總結范圍:

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

與:

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

如果您對這些進行分析,您會發現執行速度存在顯着差異。 因此,有時候表現是一個非常好的理由。

易於更新

使用可變性的一個原因是,如果您正在跟蹤一些正在進行的過程。 例如,假設我正在編輯一個大型文檔,並且有一組復雜的類來跟蹤文本的各種元素,編輯歷史,光標位置等。 現在假設用戶點擊文本的不同部分。 我是否重新創建文檔對象,復制許多字段而不是EditState字段; 使用新的ViewBoundsdocumentCursorPosition重新創建EditState 或者我在一個地方改變一個可變變量? 只要線程安全不是問題,那么只更新一個或兩個變量比復制所有內容更簡單且更不容易出錯。 如果線程安全一個問題,那么防止並發訪問可能比使用不可變方法和處理過期請求更多的工作。

計算效率

使用可變性的另一個原因是速度。 對象創建很便宜,但簡單的方法調用更便宜,對原始類型的操作也更便宜。

例如,假設我們有一個地圖,我們想要對值和值的平方求和。

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

如果你偶爾這樣做,這沒什么大不了的。 但是如果你注意到正在發生的事情,對於你首先重新創建它的每個列表元素(值),然后將它加總(加框),然后再次重新創建它(值),然后再用方塊形式再次重新創建它(地圖)然后總結一下。 這至少是六個對象創建和五個完整遍歷,只需要每個項目進行兩次加法和一次乘法。 難以置信的低效率。

您可以嘗試通過避免多次遞歸並使用折疊僅傳遞一次地圖來做得更好:

val (sum,sumsq) = ((0,0) /: xs){ case ((sum,sumsq),(_,v)) => (sum + v, sumsq + v*v) }

這要好得多,我的機器性能提高了15倍。 但是每次迭代你仍然有三個對象創建。 如果相反你

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)

你再次快兩倍,因為你減少了拳擊。 如果您將數據放在數組中並使用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)
}

在許多方面它甚至更干凈,除了你必須為你做的每一次改變復制你的對象,這可能會非常緩慢。 這些都不是特別防彈的; 因為你可能想要的

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
}

但同樣,還有很多開銷。

我發現命令式/可變式更適合動態編程算法。 如果你堅持不可靠性,那么對大多數人來說編程就更難了,而你最終會使用大量內存和/或溢出堆棧。 一個例子: 功能范例中的動態編程

一些例子:

  1. (最初是評論)任何程序都必須做一些輸入和輸出(否則,它是無用的)。 但是根據定義,輸入/輸出是副作用,如果不調用帶副作用的方法就無法完成。

  2. Scala的一個主要優點是能夠使用Java庫。 他們中的許多人依賴於具有副作用的可變對象和方法。

  3. 有時你需要一個由於范圍界定的var 有關示例,請參閱此博客文章中的Temperature4

  4. 並發編程。 如果您使用演員,發送和接收消息是副作用; 如果你使用線程,同步鎖是一個副作用,鎖是可變的; 事件驅動的並發是關於副作用的; 期貨,並發收款等是可變的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM