Solução alternativa para implementar operações em estruturas de dados duplamente vinculadas ou circulares em idiomas com dados imutáveis


11

Gostaria de aprender como criar gráficos e executar algumas operações locais sobre eles em Haskell, mas a questão não é específica para Haskell e, em vez de gráficos, podemos considerar listas duplamente vinculadas.

Pergunta: Qual seria uma maneira idiomática ou recomendada para implementar uma lista duplamente vinculada (ou outra estrutura de dados duplamente vinculada ou circular) e operações nela em uma linguagem que principalmente apóia e advoga estruturas de dados imutáveis ​​(Haskell, Clojure etc.) ? Em particular, como usar atualizações no local, formalmente proibidas pelo idioma?

Posso imaginar facilmente que, se alguma operação local for executada em uma lista duplamente vinculada (se um item for inserido, por exemplo), talvez não seja necessário copiar a lista inteira imediatamente devido à preguiça do idioma. No entanto, como a lista é duplamente vinculada, se for modificada em um só lugar, nenhum dos nós antigos poderá ser usado na nova versão da lista e eles precisarão ser marcados, copiados, coletados de maneira mais cedo ou mais tarde . Obviamente, essas são operações redundantes se apenas a cópia atualizada da lista for usada, mas adicionariam uma "sobrecarga" proporcional ao tamanho da lista.

Isso significa que, para essas tarefas, dados imutáveis ​​são simplesmente inapropriados, e linguagens declarativas funcionais sem suporte "nativo" para dados mutáveis ​​não são tão boas quanto as imperativas? Ou, há alguma solução alternativa complicada?

PS: Encontrei alguns artigos e apresentações sobre esse assunto na Internet, mas tive dificuldade em segui-los, enquanto acho que a resposta a essa pergunta não deve levar mais que um parágrafo e talvez um diagrama ... quero dizer, se houver nenhuma solução "funcional" para esse problema, a resposta provavelmente é "use C". Se houver, então, quão complicado pode ser?


Perguntas relacionadas


Citações relevantes

Linguagens de programação puramente funcionais permitem que muitos algoritmos sejam expressos de forma muito concisa, mas existem alguns algoritmos nos quais o estado atualizável no local parece desempenhar um papel crucial. Para esses algoritmos, linguagens puramente funcionais, que não possuem estado atualizável, parecem ser inerentemente ineficientes ( [Ponder, McGeer e Ng, 1988] ).

- John Launchbury e Simon Peyton Jones, threads preguiçosos do estado funcional (1994), também John Launchbury e Simon Peyton Jones, Estado em Haskell (1995). Estes artigos introduzem o STconstrutor do tipo monádico em Haskell.


4
Recomendado: Okasaki
Robert Harvey

2
Obrigado pela referência. Eu encontrei sua tese .
Alex12 #

Este artigo parece promissor: Pesquisa preguiçosa em profundidade e algoritmos de gráficos lineares em Haskell (1994), de David King e John Launchbury.
Alexey

Parece que um problema semelhante com matrizes é resolvido pelo pacote diffarray que implementa o DiffArraytipo. Olhando a fonte do pacote diffarray , vejo 91 ocorrências de unsafePerformIO. Parece que a resposta para minha pergunta é "sim, não, linguagens puramente funcionais com dados imutáveis ​​não são apropriadas para implementar algoritmos que normalmente dependem de atualizações no local".
31415 Alexey

Minha solução atual (em Haskell) é usar um dicionário ( Map, IntMapou HashMap) como um armazenamento e fazer nós contêm IDs dos nós ligados. "Todos os problemas em ciência da computação podem ser resolvidos por outro nível de indireção".
Alexey

Respostas:


6

Pode haver outras estruturas de dados imutáveis ​​eficientes que se ajustam à sua tarefa específica, mas não são tão gerais quanto uma lista duplamente vinculada (que infelizmente é propensa a erros de modificação simultâneos devido à sua mutabilidade). Se você especificar seu problema de maneira mais restrita, provavelmente poderá encontrar uma estrutura desse tipo.

A resposta geral para a travessia (relativamente) econômica de estruturas imutáveis ​​são as lentes. A idéia é que você possa manter apenas informações suficientes para reconstruir uma estrutura imutável modificada de suas partes não modificadas e da peça atualmente modificada e navegar por ela para um nó vizinho.

Outra estrutura útil é um zíper . (A parte engraçada é que a assinatura de tipo para um zíper de lente é um derivado de matemática da escola de uma assinatura de tipo da estrutura.)

Aqui estão alguns links.


1
dependendo do que for necessário, um zíper também pode ser útil
jk.

Para especificar meu problema mais estritamente, suponha que eu queira programar um sistema de reescrita de gráficos, por exemplo, um avaliador de cálculo lambda com base na reescrita de gráficos.
Alexey

1
@Alexey: Você está familiarizado com o trabalho das pessoas limpas na reescrita de gráficos? wiki.clean.cs.ru.nl/...
Giorgio

1
@ Alexey: Não que eu saiba: Clean é uma prima de Haskell que foi desenvolvida por conta própria. Também possui um mecanismo diferente para lidar com efeitos colaterais (o AFAIK é chamado de tipos únicos). Por outro lado, os desenvolvedores trabalharam muito com a reescrita de gráficos. Portanto, eles podem estar entre as melhores pessoas que conhecem tanto a reescrita de gráficos quanto a programação funcional.
Giorgio

1
Concordo que um zíper parece resolver o problema com uma lista ou uma árvore duplamente vinculada, se eu quiser navegar e modificar no local em que estou atualmente, mas não está claro o que fazer se eu quiser me concentrar em vários pontos simultaneamente e, por exemplo, troque dois elementos em dois lugares distantes. É ainda menos claro se pode ser usado com estruturas "circulares".
Alex12 #

2

Haskell não impede o uso de estruturas de dados mutáveis. Eles são fortemente desencorajados e dificultados de usar devido ao fato de que as partes do código que os utilizam devem retornar uma ação de E / S (que deve eventualmente ser vinculada à ação de E / S que é retornada pela função principal), mas isso não ocorre. torne impossível usar essas estruturas se você realmente precisar delas.

Eu sugeriria investigar o uso da memória transacional do software como um caminho a seguir. Além de fornecer uma maneira eficiente de implementar estruturas mutáveis, também fornece garantias muito úteis para a segurança do encadeamento. Veja a descrição do módulo em https://hackage.haskell.org/package/stm e a visão geral do wiki em https://wiki.haskell.org/Software_transactional_memory .


Obrigado, vou tentar aprender sobre o STM. Parece que há mais métodos em Haskell para ter mutabilidade e estadual (Eu depara com MVar, State, ST), então eu vou precisa descobrir as suas diferenças e usos pretendidos.
Alexey19:

@ Alexey: Bom ponto ST, IMO deve ser mencionado na resposta porque permite executar uma computação com estado, depois jogar fora o estado e extrair o resultado como um valor puro.
Giorgio

@Giorgio, é possível usar o Haskell's STcom STM para ter simultaneidade e estado descartável?
Alexey #

Apenas mais uma sugestão de terminologia: a ação principal de E / S composta não é " retornada pela função principal", mas é atribuída à mainvariável. :) ( mainnão até mesmo realizar uma função.)
Alexey

Entendo o seu argumento, mas ainda assim "variável" tem uma conotação na mente da maioria das pessoas como um valor simples, e não como um processo que produz um valor, e o principal é claramente melhor pensado como o último do que o primeiro. A mudança que você sugere, embora claramente tecnicamente correta, tem o potencial de confundir aqueles que não estão familiarizados com o assunto.
Jules
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.