La domanda è divisa in due parti. Il primo è concettuale. Il prossimo guarda più concretamente alla stessa domanda in Scala.
- Utilizzare solo strutture di dati immutabili in un linguaggio di programmazione rende l'implementazione di determinati algoritmi / logica intrinsecamente più costosa dal punto di vista computazionale nella pratica? Ciò attinge al fatto che l'immutabilità è un principio fondamentale dei linguaggi puramente funzionali. Ci sono altri fattori che influiscono su questo?
- Facciamo un esempio più concreto. Quicksort viene generalmente insegnato e implementato utilizzando operazioni mutabili su una struttura dati in memoria. Come si implementa una cosa del genere in modo funzionale PURO con un sovraccarico di calcolo e archiviazione comparabile alla versione modificabile. Nello specifico in Scala. Ho incluso alcuni benchmark grezzi di seguito.
Più dettagli:
Vengo da un background di programmazione imperativo (C ++, Java). Ho esplorato la programmazione funzionale, in particolare Scala.
Alcuni dei principi primari della pura programmazione funzionale:
- Le funzioni sono cittadini di prima classe.
- Le funzioni non hanno effetti collaterali e quindi gli oggetti / le strutture dati sono immutabili .
Anche se le JVM moderne sono estremamente efficienti con la creazione di oggetti e la garbage collection è molto poco costosa per oggetti di breve durata, è probabilmente ancora meglio ridurre al minimo la creazione di oggetti, giusto? Almeno in un'applicazione a thread singolo in cui la concorrenza e il blocco non sono un problema. Poiché Scala è un paradigma ibrido, si può scegliere di scrivere codice imperativo con oggetti mutabili, se necessario. Ma, come qualcuno che ha trascorso molti anni cercando di riutilizzare gli oggetti e ridurre al minimo l'allocazione. Vorrei una buona comprensione della scuola di pensiero che non lo permetterebbe nemmeno.
Come caso specifico, sono rimasto un po 'sorpreso da questo frammento di codice in questo tutorial 6 . Ha una versione Java di Quicksort seguita da un'attenta implementazione Scala dello stesso.
Ecco il mio tentativo di confrontare le implementazioni. Non ho eseguito una profilazione dettagliata. Ma la mia ipotesi è che la versione Scala sia più lenta perché il numero di oggetti allocati è lineare (uno per chiamata di ricorsione). C'è qualche possibilità che le ottimizzazioni della chiamata di coda possano entrare in gioco? Se ho ragione, Scala supporta l'ottimizzazione delle chiamate tail per le chiamate auto-ricorsive. Quindi, dovrebbe solo aiutarlo. Sto usando Scala 2.8.
Versione 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;
}
}
Versione 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 Code per confrontare le implementazioni
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"))
Risultati
Tempo in millisecondi per cinque esecuzioni consecutive
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)
concatenazione di elenchi. Tuttavia, è più breve della versione con pseudocodice;)