La pregunta tiene dos partes. El primero es conceptual. El siguiente analiza la misma pregunta de forma más concreta en Scala.
- ¿Usar solo estructuras de datos inmutables en un lenguaje de programación hace que la implementación de ciertos algoritmos / lógica sea inherentemente más costosa computacionalmente en la práctica? Esto se debe al hecho de que la inmutabilidad es un principio básico de los lenguajes puramente funcionales. ¿Hay otros factores que influyan en esto?
- Tomemos un ejemplo más concreto. La ordenación rápida generalmente se enseña e implementa mediante operaciones mutables en una estructura de datos en memoria. ¿Cómo se implementa tal cosa de una manera funcional PURA con una sobrecarga computacional y de almacenamiento comparable a la versión mutable? Específicamente en Scala. He incluido algunos puntos de referencia crudos a continuación.
Más detalles:
Vengo de una experiencia de programación imperativa (C ++, Java). He estado explorando la programación funcional, específicamente Scala.
Algunos de los principios básicos de la programación funcional pura:
- Las funciones son ciudadanos de primera.
- Las funciones no tienen efectos secundarios y, por lo tanto, los objetos / estructuras de datos son inmutables .
Aunque las JVM modernas son extremadamente eficientes con la creación de objetos y la recolección de basura es muy económica para los objetos de corta duración, probablemente sea mejor minimizar la creación de objetos, ¿verdad? Al menos en una aplicación de un solo subproceso donde la concurrencia y el bloqueo no son un problema. Dado que Scala es un paradigma híbrido, se puede optar por escribir código imperativo con objetos mutables si es necesario. Pero, como alguien que ha pasado muchos años tratando de reutilizar objetos y minimizar la asignación. Me gustaría una buena comprensión de la escuela de pensamiento que ni siquiera permitiría eso.
Como caso específico, me sorprendió un poco este fragmento de código en este tutorial 6 . Tiene una versión Java de Quicksort seguida de una elegante implementación de Scala de la misma.
Aquí está mi intento de comparar las implementaciones. No he realizado perfiles detallados. Pero, supongo que la versión de Scala es más lenta porque la cantidad de objetos asignados es lineal (uno por llamada de recursión). ¿Existe alguna posibilidad de que las optimizaciones de llamadas finales entren en juego? Si estoy en lo cierto, Scala admite optimizaciones de llamadas de cola para llamadas auto-recursivas. Entonces, solo debería ayudarlo. Estoy usando Scala 2.8.
Versión de 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;
}
}
Versión 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 <)))
}
}
Código Scala para comparar implementaciones
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"))
Resultados
Tiempo en milisegundos para cinco ejecuciones consecutivas
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)list concat. Sin embargo, es más corto que la versión de pseudocódigo;)