Como as linguagens de programação puramente funcionais lidam com os dados que mudam rapidamente?

22

Quais estruturas de dados você pode usar para obter a remoção e substituição de O (1)? Ou como você pode evitar situações em que precisa dessas estruturas?

mrpyo
fonte
2
Para aqueles de nós que estão menos familiarizados com linguagens de programação puramente funcionais, você acha que poderia fornecer um pouco mais de experiência para entendermos qual é o seu problema?
FrustratedWithFormsDesigner
4
@FrustratedWithFormsDesigner Linguagens de programação puramente funcionais exigem que todas as variáveis ​​sejam imutáveis, o que, por sua vez, requer estruturas de dados que criam novas versões de si mesmas quando "modificadas".
Doval
5
Você conhece o trabalho de Okasaki em estruturas de dados puramente funcionais?
2
Uma possibilidade é definir uma mônada para dados mutáveis ​​(veja, por exemplo, haskell.org/ghc/docs/4.08/set/sec-marray.html ). Dessa maneira, os dados mutáveis ​​são tratados da mesma forma que as E / S.
Giorgio
1
@CodesInChaos: no entanto, essas estruturas imutáveis ​​também costumam ter muito mais sobrecarga do que matrizes simples. Como resultado, não é muito uma diferença prática. É por isso que qualquer linguagem puramente funcional que visa a programação de uso geral deve ter uma maneira de usar estruturas mutáveis, de alguma maneira segura compatível com a semântica pura. A STmônada em Haskell faz isso de forma excelente.
leftaroundabout

Respostas:

32

Existe uma vasta gama de estruturas de dados que exploram a preguiça e outros truques para obter tempo constante amortizado ou até (em alguns casos limitados, como filas ) atualizações de tempo constante para muitos tipos de problemas. A tese de doutorado de Chris Okasaki "Estruturas de dados puramente funcionais" e livro com o mesmo nome é um excelente exemplo (talvez o primeiro grande), mas o campo avançou desde então . Essas estruturas de dados normalmente não são apenas puramente funcionais na interface, mas também podem ser implementadas em puro Haskell e linguagens semelhantes e são totalmente persistentes.

Mesmo sem nenhuma dessas ferramentas avançadas, as árvores de pesquisa binária balanceada simples fornecem atualizações em tempo logarítmico, de modo que a memória mutável pode ser simulada com, no pior caso, uma lentidão logarítmica.

Existem outras opções, que podem ser consideradas trapaceiras, mas são muito eficazes em relação ao esforço de implementação e ao desempenho do mundo real. Por exemplo, tipos lineares ou tipos de exclusividade permitem a atualização no local como estratégia de implementação de uma linguagem conceitualmente pura, impedindo que o programa mantenha o valor anterior (a memória que seria modificada). Isso é menos geral do que as estruturas de dados persistentes: por exemplo, você não pode criar facilmente um log de desfazer armazenando todas as versões anteriores do estado. Ainda é uma ferramenta poderosa, embora o AFAIK ainda não esteja disponível nas principais linguagens funcionais.

Outra opção para introduzir com segurança o estado mutável em uma configuração funcional é a STmônada em Haskell. Ele pode ser implementado sem mutação e, com as unsafe*funções de restrição , se comporta como se fosse apenas um invólucro sofisticado em torno da passagem implícita de uma estrutura de dados persistente (cf. State). Porém, devido a algum tipo de truque do sistema que impõe a ordem da avaliação e evita a fuga, ele pode ser implementado com segurança com mutação no local, com todos os benefícios de desempenho.

Comunidade
fonte
Também vale a pena mencionar Zíperes, permitindo que você faça alterações rápidas em um foco em uma lista ou árvore
jk.
1
@jk. Eles são mencionados no post de Teoria da Computação à qual eu vinculei. Além disso, eles são apenas uma (bem, uma classe) de muitas estruturas de dados relevantes e a discussão de todas elas está fora do escopo e é de pouca utilidade.
justo o suficiente, não seguiu os links
jk.
9

Uma estrutura mutável barata é a pilha de argumentos.

Dê uma olhada no cálculo fatorial típico do estilo SICP:

(defn fac (n accum) 
    (if (= n 1) 
        accum 
        (fac (- n 1) (* accum n)))

(defn factorial (n) (fac n 1))

Como você pode ver, o segundo argumento para facé usado como um acumulador mutável para conter o produto que muda rapidamente n * (n-1) * (n-2) * .... Porém, não existe uma variável mutável à vista e não há como alterar inadvertidamente o acumulador, por exemplo, de outro encadeamento.

Este é, obviamente, um exemplo limitado.

Você pode obter listas vinculadas imutáveis ​​com a substituição barata do nó principal (e, por extensão, qualquer parte que comece a partir do cabeçalho): basta fazer o novo cabeçalho apontar para o mesmo nó seguinte do cabeçote antigo. Isso funciona bem com muitos algoritmos de processamento de lista ( foldcom base em qualquer coisa ).

Você pode obter um desempenho bastante bom de matrizes associativas baseadas, por exemplo, em HAMTs . Logicamente, você recebe uma nova matriz associativa com alguns pares de valores-chave alterados. A implementação pode compartilhar a maioria dos dados comuns entre os objetos antigos e os recém-criados. Este não é O (1); normalmente você obtém algo logarítmico, pelo menos na pior das hipóteses. Árvores imutáveis, por outro lado, geralmente não sofrem nenhuma penalidade de desempenho em comparação com árvores mutáveis. Obviamente, isso requer alguma sobrecarga de memória, geralmente longe de proibitiva.

Outra abordagem é baseada na idéia de que, se uma árvore cai em uma floresta e ninguém a ouve, ela não precisa produzir som. Ou seja, se você puder provar que um pouco de estado mutado nunca deixa algum escopo local, poderá alterar os dados nele com segurança.

O Clojure possui transientes que são 'sombras' mutáveis ​​de estruturas de dados imutáveis ​​que não vazam para fora do escopo local. O Clean usa o Uniques para obter algo semelhante (se bem me lembro). A ferrugem ajuda a fazer coisas semelhantes com ponteiros únicos verificados estaticamente.

9000
fonte
1
+1, também por mencionar tipos exclusivos no Clean.
Giorgio
@ 9000 Acho que ouvi dizer que Haskell tem algo parecido com os transitórios de Clojure - alguém me corrija se eu estiver errado.
Paul
@paul: Eu tenho um conhecimento muito superficial de Haskell, portanto, se você pudesse fornecer minhas informações (pelo menos uma palavra-chave para o google), eu ficaria feliz em incluir uma referência à resposta.
9000
1
@ Paul Não tenho tanta certeza. Mas Haskell tem um método de criar algo semelhante aos ML refe limitá-los dentro de um certo escopo. Veja IORefou STRef. E depois, claro, há TVars e MVars que são semelhantes, mas com semântica simultâneos sãs (STM para TVars e mutex base para MVars)
Daniel Gratzer
2

O que você está pedindo é um pouco amplo demais. O (1) remoção e substituição de qual posição? A cabeça de uma sequência? A calda? Uma posição arbitrária? A estrutura de dados a ser usada depende desses detalhes. Dito isto, 2-3 Finger Trees parecem uma das estruturas de dados persistentes mais versáteis existentes:

Apresentamos 2 a 3 árvores de dedos, uma representação funcional de seqüências persistentes que suportam o acesso às extremidades em tempo constante amortizado e concatenação e divisão logarítmica no tempo no tamanho da peça menor.

(...)

Além disso, definindo a operação de divisão de uma forma geral, obtemos uma estrutura de dados de uso geral que pode servir como uma sequência, fila de prioridade, árvore de pesquisa, fila de pesquisa prioritária e muito mais.

As estruturas de dados geralmente persistentes têm desempenho logarítmico ao alterar posições arbitrárias. Isso pode ou não ser um problema, pois a constante em um algoritmo O (1) pode ser alta e a desaceleração logarítmica pode ser "absorvida" em um algoritmo geral mais lento.

Mais importante, as estruturas de dados persistentes facilitam o raciocínio sobre o seu programa, e esse deve sempre ser o seu modo padrão de operação. Você deve favorecer estruturas de dados persistentes sempre que possível e usar apenas uma estrutura de dados mutável depois de criar um perfil e determinar que a estrutura de dados persistentes é um gargalo de desempenho. Qualquer outra coisa é otimização prematura.

Doval
fonte