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

Из программирования в Scala (второе издание), внизу p.98:

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

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

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

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

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

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


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

Ответы

Ответ 1

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

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

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

Versus:

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

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

Ответ 2

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

Одной из причин использования изменчивости является то, что вы отслеживаете некоторые текущие процессы. Например, допустим, что я редактирую большой документ и имею сложный набор классов для отслеживания различных элементов текста, истории редактирования, положения курсора и т.д. Теперь предположим, что пользователь нажимает на другую часть текста. Восстановить объект документа, скопировать много полей, но не в поле EditState; воссоздайте EditState с новыми ViewBounds и 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
}

но опять-таки много накладных расходов.

Ответ 3

Я обнаружил, что императивный/изменяемый стиль лучше подходит для динамических алгоритмов программирования. Если вы настаиваете на непреклонности, сложнее запрограммировать для большинства людей, и вы в конечном итоге используете огромное количество памяти и/или переполняете стек. Один пример: Динамическое программирование в функциональной парадигме

Ответ 4

Некоторые примеры:

  • (Первоначально комментарий) Любая программа должна делать некоторые ввод и вывод (в противном случае это бесполезно). Но по определению вход/выход является побочным эффектом и не может быть выполнен без вызова методов с побочными эффектами.

  • Одним из основных преимуществ Scala является возможность использования библиотек Java. Многие из них полагаются на изменяемые объекты и методы с побочными эффектами.

  • Иногда вам требуется var из-за обзора. См. Temperature4 в этом сообщении в блоге для примера.

  • Параллельное программирование. Если вы используете актеров, отправка и получение сообщений являются побочным эффектом; если вы используете потоки, синхронизация на замках является побочным эффектом, и блокировки изменяемы; event-driven concurrency - все о побочных эффектах; фьючерсы, параллельные коллекции и т.д. изменяемы.