A pergunta divide-se em duas partes. O primeiro é conceitual. O próximo analisa a mesma questão de forma mais concreta no Scala.
- Usar apenas estruturas de dados imutáveis em uma linguagem de programação torna a implementação de certos algoritmos / lógica inerentemente mais dispendiosa em termos computacionais na prática? Isso leva ao fato de que a imutabilidade é um princípio central das linguagens puramente funcionais. Existem outros fatores que impactam isso?
- Vamos dar um exemplo mais concreto. O Quicksort é geralmente ensinado e implementado usando operações mutáveis em uma estrutura de dados na memória. Como alguém implementa tal coisa de uma forma funcional PURE com uma sobrecarga computacional e de armazenamento comparável à versão mutável. Especificamente no Scala. Incluí alguns benchmarks grosseiros abaixo.
Mais detalhes:
Eu venho de uma experiência de programação imperativa (C ++, Java). Tenho explorado a programação funcional, especificamente Scala.
Alguns dos princípios básicos da programação funcional pura:
- As funções são cidadãos de primeira classe.
- As funções não têm efeitos colaterais e, portanto, os objetos / estruturas de dados são imutáveis .
Embora as JVMs modernas sejam extremamente eficientes com a criação de objetos e a coleta de lixo seja muito barata para objetos de curta duração, provavelmente ainda é melhor minimizar a criação de objetos, certo? Pelo menos em um aplicativo de thread único em que a simultaneidade e o bloqueio não são um problema. Uma vez que Scala é um paradigma híbrido, pode-se escolher escrever código imperativo com objetos mutáveis, se necessário. Mas, como alguém que passou muitos anos tentando reutilizar objetos e minimizar a alocação. Eu gostaria de um bom entendimento da escola de pensamento que nem isso permitiria.
Como um caso específico, fiquei um pouco surpreso com este snippet de código neste tutorial 6 . Ele tem uma versão Java do Quicksort seguida por uma implementação do mesmo em Scala.
Aqui está minha tentativa de comparar as implementações. Não fiz perfis detalhados. Mas, meu palpite é que a versão Scala é mais lenta porque o número de objetos alocados é linear (um por chamada de recursão). Existe alguma chance de que as otimizações de chamada final possam entrar em ação? Se eu estiver certo, o Scala oferece suporte para otimizações de chamadas autorrecursivas. Então, isso só deveria estar ajudando. Estou usando o Scala 2.8.
Versão 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;
}
}
Versão 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 implementações
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
Tempo em milissegundos para cinco execuções consecutivas
Immutable/Functional/Scala 467 178 184 187 183
Mutable/Imperative/Java 51 14 12 12 12
O(n)
concat de lista. É mais curto do que a versão em pseudocódigo;)