Вопрос состоит из двух частей. Первый концептуальный. Далее мы рассмотрим тот же вопрос более конкретно в Scala.
- Делает ли использование только неизменяемых структур данных на языке программирования выполнение определенных алгоритмов / логики более затратным с точки зрения вычислений на практике? Это приводит к тому факту, что неизменяемость является основным принципом чисто функциональных языков. Есть ли другие факторы, влияющие на это?
- Возьмем более конкретный пример. Быстрая сортировка обычно преподается и реализуется с использованием изменяемых операций над структурой данных в памяти. Как реализовать такую вещь функциональным образом PURE с сопоставимыми вычислительными затратами и затратами на хранение по сравнению с изменяемой версией. В частности, в Scala. Ниже я привел несколько грубых тестов.
Подробнее:
Я вырос в императивном программировании (C ++, Java). Я изучаю функциональное программирование, в частности Scala.
Некоторые из основных принципов чистого функционального программирования:
- Функции - граждане первого класса.
- Функции не имеют побочных эффектов и, следовательно, объекты / структуры данных неизменяемы .
Несмотря на то, что современные JVM чрезвычайно эффективны при создании объектов, а сборка мусора для недолговечных объектов обходится очень недорого, вероятно, все же лучше минимизировать создание объектов, верно? По крайней мере, в однопоточном приложении, где параллелизм и блокировка не являются проблемой. Поскольку Scala - это гибридная парадигма, при необходимости можно написать императивный код с изменяемыми объектами. Но как человек, который потратил много лет на попытки повторно использовать объекты и минимизировать выделение памяти. Я хотел бы хорошо понять школу мысли, которая бы даже этого не допустила.
В конкретном случае я был немного удивлен этим фрагментом кода в этом уроке 6 . У него есть Java-версия Quicksort, за которой следует изящная реализация на Scala.
Вот моя попытка протестировать реализации. Я не делал подробного профилирования. Но я предполагаю, что версия Scala медленнее, потому что количество выделяемых объектов линейно (по одному на вызов рекурсии). Есть ли шанс, что в игру вступят оптимизации хвостовых вызовов? Если я прав, Scala поддерживает оптимизацию хвостовых вызовов для саморекурсивных вызовов. Так что это должно только помочь. Я использую Scala 2.8.
Версия Java
public class QuickSortJ {
public static void sort(int[] xs) {
sort(xs, 0, xs.length -1 );
}
static void sort(int[] xs, int l, int r) {
if (r >= l) return;
int pivot = xs[l];
int a = l; int b = r;
while (a <= b){
while (xs[a] <= pivot) a++;
while (xs[b] > pivot) b--;
if (a < b) swap(xs, a, b);
}
sort(xs, l, b);
sort(xs, a, r);
}
static void swap(int[] arr, int i, int j) {
int t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
}
Версия Scala
object QuickSortS {
def sort(xs: Array[Int]): Array[Int] =
if (xs.length <= 1) xs
else {
val pivot = xs(xs.length / 2)
Array.concat(
sort(xs filter (pivot >)),
xs filter (pivot ==),
sort(xs filter (pivot <)))
}
}
Код Scala для сравнения реализаций
import java.util.Date
import scala.testing.Benchmark
class BenchSort(sortfn: (Array[Int]) => Unit, name:String) extends Benchmark {
val ints = new Array[Int](100000);
override def prefix = name
override def setUp = {
val ran = new java.util.Random(5);
for (i <- 0 to ints.length - 1)
ints(i) = ran.nextInt();
}
override def run = sortfn(ints)
}
val benchImmut = new BenchSort( QuickSortS.sort , "Immutable/Functional/Scala" )
val benchMut = new BenchSort( QuickSortJ.sort , "Mutable/Imperative/Java " )
benchImmut.main( Array("5"))
benchMut.main( Array("5"))
Полученные результаты
Время в миллисекундах для пяти последовательных запусков
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)списков. Это короче, чем версия с псевдокодом;)