Питання складається з двох частин. Перший - концептуальний. Наступний розглядає це питання більш конкретно у Scala.
- Чи робить використання лише незмінних структур даних у мові програмування реалізацію певних алгоритмів / логіки, як правило, дорожче обчислювальних на практиці? Це призводить до того, що незмінність є основним принципом суто функціональних мов. Чи існують інші фактори, які впливають на це?
- Візьмемо більш конкретний приклад. Як правило, Quicksort викладається та реалізується з використанням змінних операцій над структурою даних в пам'яті. Як реалізувати таку річ ЧИСТИМ функціональним способом із порівнянними накладними витратами на обчислення та зберігання до змінної версії. Зокрема в 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)
списку конкатів. Однак він коротший за версію псевдокоду;)