Когда императивный стиль подходит лучше?
Из " Программирование в Scala" (второе издание), внизу стр.98:
Сбалансированное отношение к программистам Scala
Предпочитайте значения, неизменяемые объекты и методы без побочных эффектов. Дотянись до них первым. Используйте переменные, изменяемые объекты и методы с побочными эффектами, когда у вас есть для них особая потребность и обоснование.
На предыдущих страницах объясняется, почему предпочитать vals, неизменяемые объекты и методы без побочных эффектов, поэтому это предложение имеет смысл.
Но второе предложение:"Используйте переменные, изменяемые объекты и методы с побочными эффектами, когда у вас есть для них особая необходимость и обоснование". не так хорошо объяснено.
Итак, мой вопрос:
В чем обоснованность или особая необходимость использования переменных, изменяемых объектов и методов с побочным эффектом?
Ps: Было бы здорово, если бы кто-то мог привести несколько примеров для каждого из них (помимо объяснения).
4 ответа
Во многих случаях функциональное программирование повышает уровень абстракции и, следовательно, делает ваш код более лаконичным и простым / быстрым для написания и понимания. Но бывают ситуации, когда результирующий байт-код не может быть настолько оптимизирован (быстр), как для императивного решения.
В настоящее время (Scala 2.9.1) хорошим примером является суммирование диапазонов:
(1 to 1000000).foldLeft(0)(_ + _)
Против:
var x = 1
var sum = 0
while (x <= 1000000) {
sum += x
x += 1
}
Если вы профилируете их, вы заметите существенную разницу в скорости выполнения. Так что иногда производительность - это действительно хорошее оправдание.
Легкость незначительных обновлений
Одна из причин использования изменчивости заключается в том, что вы отслеживаете какой-то текущий процесс. Например, предположим, что я редактирую большой документ и у меня есть сложный набор классов для отслеживания различных элементов текста, истории редактирования, положения курсора и так далее. Теперь предположим, что пользователь нажимает на другую часть текста. Я воссоздаю объект документа, копируя много полей, но не 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
}
но опять же, есть много накладных расходов.
Я обнаружил, что императивный / изменчивый стиль лучше подходит для алгоритмов динамического программирования. Если вы настаиваете на неизменности, для большинства людей программирование сложнее, и в итоге вы используете огромное количество памяти и / или переполняете стек. Один пример: динамическое программирование в функциональной парадигме
Некоторые примеры:
(Первоначально комментарий) Любая программа должна делать ввод и вывод (иначе это бесполезно). Но по определению ввод / вывод является побочным эффектом и не может быть осуществлен без вызова методов с побочными эффектами.
Одним из основных преимуществ Scala является возможность использования библиотек Java. Многие из них полагаются на изменяемые объекты и методы с побочными эффектами.
Иногда вам нужно
var
из-за обзора. УвидетьTemperature4
в этом сообщении в блоге для примера.Параллельное программирование. Если вы используете актеров, отправка и получение сообщений являются побочным эффектом; если вы используете потоки, синхронизация с блокировками является побочным эффектом, а блокировки являются изменяемыми; управляемый событиями параллелизм - все о побочных эффектах; фьючерсы, параллельные коллекции и т. д. являются изменчивыми.