当命令式的风格更合适?

时间:2012-02-01 10:03:21

标签: scala functional-programming imperative-programming

来自 Scala编程(第二版),p.98的底部:

  

Scala程序员的平衡态度

     

首选vals,不可变对象和没有副作用的方法。   首先到达他们。当您有特定的需要和理由时,请使用变量,可变对象和带副作用的方法。

前面几页解释了为什么更喜欢val,不可变对象和没有副作用的方法,所以这句话很有意义。

但第二句:“当你有特殊的需要和理由时,使用变量,可变对象和副作用的方法。”没有解释得这么好。

所以我的问题是:

使用变量,可变对象和有副作用的方法的理由或具体需要是什么?


P.s。:如果有人可以为每个人提供一些例子(除了解释),那将是很棒的。

4 个答案:

答案 0 :(得分:16)

在许多情况下,函数式编程可以提高抽象级别,从而使您的代码更简洁,更容易/更快地编写和理解。但是在某些情况下,生成的字节码不能像命令式解决方案那样优化(快速)。

目前(Scala 2.9.1)一个很好的例子是总结范围:

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

对战:

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

如果您对这些进行分析,您会发现执行速度存在显着差异。因此,有时候表现是一个非常好的理由。

答案 1 :(得分:8)

轻松更新

使用可变性的一个原因是,如果您正在跟踪某些正在进行的过程。例如,假设我正在编辑一个大型文档,并且有一组复杂的类来跟踪文本的各种元素,编辑历史,光标位置等。现在假设用户点击文本的不同部分。我是否重新创建文档对象,复制许多字段但不复制EditState字段;使用新的EditStateViewBounds重新创建documentCursorPosition?或者我在一个地方改变一个可变变量? 只要线程安全不是问题那么只更新一个或两个变量比复制所有内容更简单且更不容易出错。如果线程安全 是一个问题,那么防止并发访问可能比使用不可变方法和处理过期请求更多的工作。

计算效率

使用可变性的另一个原因是速度。对象创建很便宜,但简单的方法调用更便宜,对原始类型的操作也更便宜。

我们假设,例如,我们有一个地图,我们想要对值的值和方块求和。

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
}

但同样,还有很多开销。

答案 2 :(得分:5)

我发现命令式/可变式风格更适合动态编程算法。如果你坚持不可靠性,那么对大多数人来说编程就更难了,而你最终会使用大量内存和/或溢出堆栈。一个例子:Dynamic programming in the functional paradigm

答案 3 :(得分:3)

一些例子:

  1. (最初评论)任何程序都必须做一些输入和输出(否则,它是无用的)。但是根据定义,输入/输出是副作用,如果不调用带副作用的方法就无法完成。

  2. Scala的一个主要优点是能够使用Java库。他们中的许多人依赖于具有副作用的可变对象和方法。

  3. 由于范围界定,有时您需要var。有关示例,请参阅this blog post中的Temperature4

  4. 并发编程。如果您使用演员,发送和接收消息是副作用;如果你使用线程,同步锁是一个副作用,锁是可变的;事件驱动的并发是关于副作用的;期货,并发收款等是可变的。