Na programação funcional, uma vez que quase toda estrutura de dados é imutável, quando o estado precisa mudar, uma nova estrutura é criada. Isso significa muito mais uso de memória? Conheço bem o paradigma de programação orientada a objetos, agora estou tentando aprender sobre o paradigma de programação funcional. O conceito de tudo ser imutável me confunde. Parece que um programa usando estruturas imutáveis exigiria muito mais memória do que um programa com estruturas mutáveis. Estou olhando para isso da maneira certa?
functional-programming
Jbemmz
fonte
fonte
Respostas:
A única resposta correta para isso é "às vezes". Existem muitos truques que as linguagens funcionais podem usar para evitar desperdiçar memória. A imutabilidade facilita o compartilhamento de dados entre funções e até entre estruturas de dados, pois o compilador pode garantir que os dados não serão modificados. Linguagens funcionais tendem a incentivar o uso de estruturas de dados que podem ser usadas eficientemente como estruturas imutáveis (por exemplo, árvores em vez de tabelas de hash). Se você adicionar preguiça à mistura, como muitas linguagens funcionais, isso adiciona novas maneiras de economizar memória (também adiciona novas maneiras de desperdiçar memória, mas não vou entrar nisso).
fonte
Isso depende da estrutura dos dados, das mudanças exatas que você executou e, em alguns casos, do otimizador. Como um exemplo, vamos considerar anexar a uma lista:
Aqui, o requisito adicional de memória é constante - assim como o custo de tempo de execução da chamada
prepend
. Por quê? Porqueprepend
simplesmente cria uma nova célula que tem42
como cabeça elist1
cauda. Não é necessário copiar ou iterarlist2
para conseguir isso. Ou seja, exceto a memória necessária para armazenar42
,list2
reutiliza a mesma memória usada porlist1
. Como as duas listas são imutáveis, esse compartilhamento é perfeitamente seguro.Da mesma forma, ao trabalhar com estruturas de árvore balanceadas, a maioria das operações requer apenas uma quantidade logarítmica de espaço adicional, pois tudo, exceto um caminho da árvore, pode ser compartilhado.
Para matrizes, a situação é um pouco diferente. É por isso que, em muitas linguagens FP, matrizes não são tão comumente usadas. No entanto, se você fizer algo parecido
arr2 = map(f, arr1)
earr1
nunca for usado novamente após essa linha, um otimizador inteligente pode realmente criar código que sofre mutação emarr1
vez de criar uma nova matriz (sem afetar o comportamento do programa). Nesse caso, o desempenho será como em uma linguagem imperativa, é claro.fonte
Implementações ingênuas realmente exporiam esse problema - quando você cria uma nova estrutura de dados em vez de atualizar uma existente, é necessário ter alguma sobrecarga.
Idiomas diferentes têm maneiras diferentes de lidar com isso, e existem alguns truques que a maioria deles usa.
Uma estratégia é a coleta de lixo . No momento em que a nova estrutura foi criada, ou logo após, as referências à estrutura antiga ficam fora do escopo, e o coletor de lixo a coleta instantaneamente ou em breve, dependendo do algoritmo do GC. Isso significa que, embora ainda haja uma sobrecarga, ela é apenas temporária e não cresce linearmente com a quantidade de dados.
Outro é escolher diferentes tipos de estruturas de dados. Onde matrizes são a estrutura de dados de lista de acesso em linguagens imperativas (geralmente agrupadas em algum tipo de contêiner de realocação dinâmica, como
std::vector
em C ++), as linguagens funcionais geralmente preferem listas vinculadas. Com uma lista vinculada, uma operação de pré-adição ('contras') pode reutilizar a lista existente como a cauda da nova lista; portanto, tudo o que realmente é alocado é o novo cabeçalho da lista. Existem estratégias semelhantes para outros tipos de estruturas de dados - conjuntos, árvores, etc.E depois há uma avaliação preguiçosa, à Haskell. A idéia é que as estruturas de dados que você cria não são totalmente criadas imediatamente; em vez disso, eles são armazenados como "thunks" (você pode pensar nelas como receitas para construir o valor quando necessário). Somente quando o valor é necessário é que o thunk é expandido para um valor real. Isso significa que a alocação de memória pode ser adiada até que a avaliação seja necessária e, nesse ponto, vários thunks podem ser combinados em uma alocação de memória.
fonte
Eu só sei um pouco sobre Clojure e suas estruturas de dados imutáveis .
Graficamente, podemos representar algo como isto:
fonte
Além do que foi dito em outras respostas, gostaria de mencionar a linguagem de programação limpa, que suporta os chamados tipos únicos . Não conheço essa linguagem, mas suponho que tipos únicos suportem algum tipo de "atualização destrutiva".
Em outras palavras, embora a semântica da atualização de um estado seja a criação de um novo valor a partir de um antigo aplicando uma função, a restrição de exclusividade pode permitir que o compilador reutilize objetos de dados internamente porque sabe que o valor antigo não será referenciado mais no programa após a produção do novo valor.
Para obter mais detalhes, consulte, por exemplo, a página inicial limpa e este artigo da wikipedia.
fonte