Когда императивный стиль подходит лучше?

с программирование в Scala (второе издание), нижняя часть р. 98:

сбалансированное отношение к программистам Scala

предпочитайте vals, неизменяемые объекты и методы без побочных эффектов. Сначала дотянись до них. Используйте vars, изменяемые объекты и методы с побочными эффектами, когда у вас есть конкретная потребность и обоснование для них.

на предыдущих страницах объясняется, почему предпочитают vals, неизменяемые объекты и методы без побочных эффектов, поэтому это предложение имеет смысл.

но второе предложение: "используйте vars, изменяемые объекты и методы с побочными эффектами, когда у вас есть конкретная потребность и оправдание для них.- это не так хорошо объясняется.

Итак, мой вопрос:

что такое обоснование или конкретная необходимость использования vars, изменяемых объектов и методов с побочным эффектом?


P. s.: Было бы здорово, если бы кто-то мог приведите несколько примеров для каждого из них (помимо объяснения).

4 ответов


во многих случаях функциональное программирование повышает уровень абстракции и, следовательно, делает ваш код более кратким и проще/быстрее написать и понять. Но есть ситуации, когда результирующий байт-код не может быть оптимизирован (быстро), как для императивного решения.

В настоящее время (Scala 2.9.1) одним из хороших примеров является суммирование диапазонов:

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

Versus:

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

Если вы профилируете их, вы заметите значительную разницу в выполнении скорость. Поэтому иногда производительность-действительно хорошее оправдание.


простота незначительных обновлений

одна из причин использования изменчивости - если вы отслеживаете какой-то текущий процесс. Например, предположим, что я редактирую большой документ и имею сложный набор классов для отслеживания различных элементов текста,истории редактирования, положения курсора и т. д. Теперь предположим, что пользователь нажимает на другую часть текста. Я воссоздаю объект документа, копируя много полей, но не EditState поле; воссоздавать EditState с новым ViewBounds и documentCursorPosition? Или я изменяю изменяемую переменную в одном месте? пока безопасность потока не является проблемой тогда это намного проще и менее подвержено ошибкам, чтобы просто обновить переменную или две, чем скопировать все. Если безопасность потока is проблема, тогда защита от параллельного доступа может быть больше работы, чем использование неизменяемого подхода и работа с устаревшими запросами.

вычислительная эффективность

другой причина использования изменчивости - скорость. Создание объектов дешево, но простые вызовы методов дешевле, а операции с примитивными типами еще дешевле.

предположим, например, что у нас есть карта, и мы хотим суммировать значения и квадраты значений.

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) }

и это намного лучше, с примерно 15x лучшей производительностью на моей машине. Но вы еще есть три объекта творения каждую итерацию. Если вместо

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. параллельное программирование. Если вы используете акторов, отправка и получение сообщений являются побочным эффектом; если вы используете потоки, синхронизация блокировок является побочным эффектом, а блокировки изменяемы;управляемый событиями параллелизм - это побочные эффекты; фьючерсы, параллельные коллекции и т. д. изменчивы.