É fácil o suficiente representar uma árvore ou lista em haskell usando tipos de dados algébricos. Mas como você representaria tipograficamente um gráfico? Parece que você precisa ter ponteiros. Eu estou supondo que você poderia ter algo como
type Nodetag = String
type Neighbours = [Nodetag]
data Node a = Node a Nodetag Neighbours
E isso seria viável. No entanto, parece um pouco dissociado; Os links entre diferentes nós na estrutura não "parecem" tão sólidos quanto os links entre os elementos anteriores e seguintes atuais em uma lista, ou os pais e filhos de um nó em uma árvore. Eu tenho um pressentimento de que fazer manipulações algébricas no gráfico, como eu defini, seria um pouco prejudicado pelo nível de indireção introduzido através do sistema de tags.
É principalmente esse sentimento de dúvida e percepção de deselegância que me leva a fazer essa pergunta. Existe uma maneira melhor / mais matematicamente elegante de definir gráficos em Haskell? Ou eu me deparei com algo inerentemente difícil / fundamental? Estruturas de dados recursivas são boas, mas isso parece ser outra coisa. Uma estrutura de dados auto-referencial em um sentido diferente de como as árvores e as listas são auto-referenciais. É como se listas e árvores fossem auto-referenciais no nível de tipo, mas os gráficos são auto-referenciais no nível de valor.
Então, o que realmente está acontecendo?
fonte
fgl
pacote foi desenvolvido a partir disso.Respostas:
Também acho estranho tentar representar estruturas de dados com ciclos em uma linguagem pura. São os ciclos que realmente são o problema; porque os valores podem ser compartilhados, qualquer ADT que possa conter um membro do tipo (incluindo listas e árvores) é realmente um DAG (gráfico acíclico direcionado). A questão fundamental é que, se você tiver os valores A e B, com A contendo B e B contendo A, nenhum deles poderá ser criado antes que o outro exista. Como Haskell é preguiçoso, você pode usar um truque conhecido como Amarrar o nó para contornar isso, mas isso faz meu cérebro doer (porque ainda não fiz muito disso). Eu fiz mais da minha programação substancial em Mercury do que Haskell até agora, e Mercury é rigoroso para que dar nó não ajuda.
Geralmente, quando me deparei com isso antes, acabei de recorrer a indireções adicionais, como você sugere; geralmente usando um mapa de IDs para os elementos reais e fazendo com que os elementos contenham referências aos IDs em vez de outros elementos. A principal coisa que eu não gostei de fazer isso (além da óbvia ineficiência) é que parecia mais frágil, introduzindo os possíveis erros de procurar um ID que não existe ou tentar atribuir o mesmo ID a mais de um elemento. É possível escrever código para que esses erros não ocorram, é claro, e até ocultá-lo atrás de abstrações para que os únicos lugares onde esses erros possam ocorrer sejam limitados. Mas ainda há mais uma coisa a se enganar.
No entanto, um rápido google no "gráfico Haskell" me levou a http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , que parece uma leitura interessante.
fonte
Na resposta de shang, você pode ver como representar um gráfico usando a preguiça. O problema com essas representações é que elas são muito difíceis de mudar. O truque para dar nó é útil apenas se você for criar um gráfico uma vez e depois nunca mudar.
Na prática, se eu realmente quiser fazer algo com meu gráfico, uso as mais representações de pedestres:
Se você estiver alterando ou editando o gráfico com frequência, recomendo usar uma representação baseada no zíper de Huet. Essa é a representação usada internamente no GHC para gráficos de controle de fluxo. Você pode ler sobre isso aqui:
Um gráfico de controle-fluxo aplicável com base no zíper de Huet
Hoopl: uma biblioteca modular e reutilizável para análise e transformação de fluxo de dados
fonte
Como Ben mencionou, os dados cíclicos em Haskell são construídos por um mecanismo chamado "amarrar o nó". Na prática, isso significa que escrevemos declarações mutuamente recursivas usando
let
orwhere
oruses, o que funciona porque as partes mutuamente recursivas são avaliadas preguiçosamente.Aqui está um exemplo de tipo de gráfico:
Como você pode ver, usamos
Node
referências reais em vez de indiretas. Veja como implementar uma função que constrói o gráfico a partir de uma lista de associações de rótulos.Nós coletamos uma lista de
(nodeLabel, [adjacentLabel])
pares e construímos osNode
valores reais por meio de uma lista de pesquisa intermediária (que faz o nó real). O truque é quenodeLookupList
(que tem o tipo[(a, Node a)]
) é construído usandomkNode
, que por sua vez se refere aonodeLookupList
para encontrar os nós adjacentes.fonte
É verdade que os gráficos não são algébricos. Para lidar com esse problema, você tem algumas opções:
Int
) e referindo-se a eles indiretamente e não algebricamente. Isso pode ser significativamente mais conveniente, tornando o tipo abstrato e fornecendo uma interface que manipula o indireto para você. Essa é a abordagem adotada por, por exemplo, fgl e outras bibliotecas práticas de gráficos no Hackage.Portanto, existem prós e contras em cada uma das opções acima. Escolha o que lhe parecer melhor.
fonte
Alguns outros mencionaram brevemente
fgl
os Gráficos Indutivos e os Algoritmos de Gráficos Funcionais de Martin Erwig , mas provavelmente vale a pena escrever uma resposta que realmente dê uma idéia dos tipos de dados por trás da abordagem da representação indutiva.Em seu artigo, Erwig apresenta os seguintes tipos:
(A representação em
fgl
é um pouco diferente e faz bom uso de classes - mas a ideia é essencialmente a mesma.)Erwig está descrevendo uma multigraph na qual nós e arestas têm rótulos e na qual todas as arestas são direcionadas. A
Node
tem um rótulo de algum tipoa
; uma aresta tem um rótulo de algum tipob
. AContext
é simplesmente (1) uma lista de arestas rotuladas apontando para um nó específico, (2) o nó em questão, (3) o rótulo do nó e (4) a lista de arestas rotuladas apontando para o nó. AGraph
pode então ser concebido indutivamente como umEmpty
ou como umContext
(com&
) fundido em um existenteGraph
.Como observa Erwig, não podemos gerar livremente um
Graph
comEmpty
e&
, como podemos gerar uma lista com os construtoresCons
eNil
, ou umTree
comLeaf
eBranch
. Além disso, ao contrário das listas (como outros já mencionaram), não haverá nenhuma representação canônica de aGraph
. Essas são diferenças cruciais.No entanto, o que torna essa representação tão poderosa e tão semelhante às representações típicas de listas e árvores de Haskell é que o
Graph
tipo de dados aqui é definido indutivamente . O fato de uma lista ser definida indutivamente é o que nos permite fazer uma correspondência tão sucinta de padrões, processar um único elemento e processar recursivamente o restante da lista; igualmente, a representação indutiva de Erwig nos permite processar recursivamente um gráfico, um deContext
cada vez. Essa representação de um gráfico se presta a uma definição simples de uma maneira de mapear sobre um gráfico (gmap
), bem como a uma maneira de executar dobras desordenadas sobre gráficos (ufold
).Os outros comentários nesta página são ótimos. A principal razão pela qual escrevi esta resposta, no entanto, é que, quando leio frases como "gráficos não são algébricos", temo que alguns leitores inevitavelmente saiam com a impressão (errônea) de que ninguém encontrou uma boa maneira de representar gráficos. em Haskell de uma maneira que permita a correspondência de padrões neles, mapeando-os, dobrando-os ou geralmente fazendo o tipo de coisa legal e funcional que estamos acostumados a fazer com listas e árvores.
fonte
Eu sempre gostei da abordagem de Martin Erwig em "Gráficos Indutivos e Algoritmos de Gráficos Funcionais", que você pode ler aqui . FWIW, uma vez eu escrevi uma implementação do Scala também, consulte https://github.com/nicolast/scalagraphs .
fonte
Qualquer discussão sobre representação de gráficos em Haskell precisa de uma menção à biblioteca de reificação de dados de Andy Gill (aqui está o artigo ).
A representação no estilo "amarrar o nó" pode ser usada para criar DSLs muito elegantes (veja o exemplo abaixo). No entanto, a estrutura de dados é de uso limitado. A biblioteca de Gill permite o melhor dos dois mundos. Você pode usar uma DSL "atar o nó", mas depois converter o gráfico baseado em ponteiro em um gráfico baseado em etiqueta para poder executar os algoritmos de sua escolha.
Aqui está um exemplo simples:
Para executar o código acima, você precisará das seguintes definições:
Quero enfatizar que este é um DSL simplista, mas o céu é o limite! Eu projetei uma DSL muito abrangente, incluindo uma sintaxe agradável de árvore para que um nó transmitisse um valor inicial para alguns de seus filhos e muitas funções de conveniência para construir tipos de nós específicos. Obviamente, o tipo de dados Node e as definições mapDeRef estavam muito mais envolvidas.
fonte
Eu gosto dessa implementação de um gráfico tirado daqui
fonte