Die Frage besteht aus zwei Teilen. Der erste ist konzeptionell. Der nächste befasst sich konkreter mit der gleichen Frage in Scala.
- Macht die Verwendung nur unveränderlicher Datenstrukturen in einer Programmiersprache die Implementierung bestimmter Algorithmen / Logik in der Praxis von Natur aus rechenintensiver? Dies beruht auf der Tatsache, dass Unveränderlichkeit ein zentraler Grundsatz rein funktionaler Sprachen ist. Gibt es andere Faktoren, die dies beeinflussen?
- Nehmen wir ein konkreteres Beispiel. Quicksort wird im Allgemeinen unter Verwendung veränderlicher Operationen an einer speicherinternen Datenstruktur gelehrt und implementiert. Wie implementiert man so etwas auf PURE-funktionale Weise mit einem vergleichbaren Rechen- und Speicheraufwand wie die veränderbare Version? Speziell in Scala. Ich habe unten einige grobe Benchmarks aufgeführt.
Mehr Details:
Ich komme aus einem zwingenden Programmierhintergrund (C ++, Java). Ich habe mich mit funktionaler Programmierung befasst, insbesondere mit Scala.
Einige der Hauptprinzipien der reinen funktionalen Programmierung:
- Funktionen sind erstklassige Bürger.
- Funktionen haben keine Nebenwirkungen und daher sind Objekte / Datenstrukturen unveränderlich .
Auch wenn moderne JVMs sind extrem effizient mit Objekterstellung und Garbage Collection ist sehr preiswert für kurzlebige Objekte, ist es wahrscheinlich noch besser Objekterstellung richtig zu minimieren? Zumindest in einer Single-Threaded-Anwendung, in der Parallelität und Sperren kein Problem darstellen. Da Scala ein hybrides Paradigma ist, kann man bei Bedarf imperativen Code mit veränderlichen Objekten schreiben. Aber als jemand, der viele Jahre damit verbracht hat, Objekte wiederzuverwenden und die Zuordnung zu minimieren. Ich hätte gerne ein gutes Verständnis der Denkschule, das das nicht einmal zulässt.
Als spezieller Fall war ich ein wenig überrascht von diesem Code-Snippet in diesem Tutorial 6 . Es hat eine Java-Version von Quicksort, gefolgt von einer ordentlich aussehenden Scala-Implementierung derselben.
Hier ist mein Versuch, die Implementierungen zu bewerten. Ich habe keine detaillierte Profilerstellung durchgeführt. Ich vermute jedoch, dass die Scala-Version langsamer ist, da die Anzahl der zugewiesenen Objekte linear ist (eines pro Rekursionsaufruf). Gibt es eine Möglichkeit, dass Tail-Call-Optimierungen ins Spiel kommen können? Wenn ich recht habe, unterstützt Scala Tail-Call-Optimierungen für selbstrekursive Anrufe. Es sollte also nur helfen. Ich benutze Scala 2.8.
Java-Version
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-Version
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-Code zum Vergleichen von Implementierungen
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"))
Ergebnisse
Zeit in Millisekunden für fünf aufeinanderfolgende Läufe
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)
Listenkonzentration verwenden. Es ist jedoch kürzer als die Pseudocode-Version;)