La question est en deux parties. Le premier est conceptuel. Le suivant aborde la même question plus concrètement dans Scala.
- L'utilisation uniquement de structures de données immuables dans un langage de programmation rend-elle la mise en œuvre de certains algorithmes / logiques intrinsèquement plus coûteuse en calcul? Cela tient au fait que l'immuabilité est un principe fondamental des langages purement fonctionnels. Y a-t-il d'autres facteurs qui ont un impact sur cela?
- Prenons un exemple plus concret. Quicksort est généralement enseigné et implémenté à l'aide d'opérations mutables sur une structure de données en mémoire. Comment implémenter une telle chose de manière fonctionnelle PURE avec une surcharge de calcul et de stockage comparable à la version mutable. Plus précisément dans Scala. J'ai inclus quelques repères bruts ci-dessous.
Plus de détails:
Je viens d'un fond de programmation impératif (C ++, Java). J'ai exploré la programmation fonctionnelle, en particulier Scala.
Certains des principes fondamentaux de la programmation fonctionnelle pure:
- Les fonctions sont des citoyens de première classe.
- Les fonctions n'ont pas d'effets secondaires et donc les objets / structures de données sont immuables .
Même si les JVM modernes sont extrêmement efficaces avec la création d'objets et le garbage collection est très peu coûteux pour les objets de courte durée, il est probablement toujours préférable de minimiser la création d'objets, n'est-ce pas? Au moins dans une application à un seul thread où la concurrence et le verrouillage ne sont pas un problème. Puisque Scala est un paradigme hybride, on peut choisir d'écrire du code impératif avec des objets mutables si nécessaire. Mais, en tant que personne qui a passé de nombreuses années à essayer de réutiliser des objets et de minimiser l'allocation. J'aimerais avoir une bonne compréhension de l'école de pensée qui ne permettrait même pas cela.
En tant que cas particulier, j'ai été un peu surpris par cet extrait de code dans ce tutoriel 6 . Il a une version Java de Quicksort suivie d'une implémentation Scala soignée de la même chose.
Voici ma tentative de comparer les implémentations. Je n'ai pas fait de profilage détaillé. Mais, je suppose que la version Scala est plus lente car le nombre d'objets alloués est linéaire (un par appel de récursivité). Y a-t-il une chance que les optimisations des appels de queue puissent entrer en jeu? Si j'ai raison, Scala prend en charge les optimisations des appels de queue pour les appels auto-récursifs. Donc, cela ne devrait que l'aider. J'utilise Scala 2.8.
Version 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;
}
}
Version 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 <)))
}
}
Code Scala pour comparer les implémentations
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"))
Résultats
Temps en millisecondes pour cinq exécutions consécutives
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)
liste concat. C'est plus court que la version pseudocode cependant;)