Como você representa um gráfico em Haskell?

125

É 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?

TheIronKnuckle
fonte
12
Você pode estar interessado no artigo de Martin Erwig sobre algoritmos funcionais de gráficos: web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01 . O fglpacote foi desenvolvido a partir disso.
John L
A página de problemas do Haskell 99 mostra alguns exemplos de gráficos usados ​​em um contexto de solução de problemas. Ele também possui uma breve introdução sobre diferentes representações.
precisa saber é o seguinte

Respostas:

47

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.

Ben
fonte
62

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:

  • Lista de arestas
  • Lista de adjacências
  • Atribua um rótulo exclusivo a cada nó, use o rótulo em vez de um ponteiro e mantenha um mapa finito de rótulos a nós

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:

Norman Ramsey
fonte
2
Outro problema com o nó é que é muito fácil desamarrar acidentalmente e desperdiçar muito espaço.
23912 hugomg
Parece que algo está errado com o site da Tuft (pelo menos no momento), e nenhum desses links funciona atualmente. Eu consegui encontrar algumas espelhos alternativos para estes: Um Applicative Controle-Flow gráfico Baseada em Zipper de Huet , Hoopl: A Modular, biblioteca reutilizável para Análise de Fluxo de Dados e Transformação
gntskn
37

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 letor whereoruses, o que funciona porque as partes mutuamente recursivas são avaliadas preguiçosamente.

Aqui está um exemplo de tipo de gráfico:

import Data.Maybe (fromJust)

data Node a = Node
    { label    :: a
    , adjacent :: [Node a]
    }

data Graph a = Graph [Node a]

Como você pode ver, usamos Nodereferê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.

mkGraph :: Eq a => [(a, [a])] -> Graph a
mkGraph links = Graph $ map snd nodeLookupList where

    mkNode (lbl, adj) = (lbl, Node lbl $ map lookupNode adj)

    nodeLookupList = map mkNode links

    lookupNode lbl = fromJust $ lookup lbl nodeLookupList

Nós coletamos uma lista de (nodeLabel, [adjacentLabel])pares e construímos os Nodevalores reais por meio de uma lista de pesquisa intermediária (que faz o nó real). O truque é que nodeLookupList(que tem o tipo [(a, Node a)]) é construído usando mkNode, que por sua vez se refere ao nodeLookupListpara encontrar os nós adjacentes.

shang
fonte
20
Você também deve mencionar que essa estrutura de dados não é capaz de descrever gráficos. Apenas descreve seus desdobramentos. (infinitos desdobramentos no espaço finito, mas ainda assim ...)
Rotsor
1
Uau. Não tive tempo de examinar todas as respostas em detalhes, mas direi que explorar uma avaliação preguiçosa como essa parece que você estaria patinando em gelo fino. Quão fácil seria cair em uma recursão infinita? Ainda coisas incríveis e são muito melhores do que o tipo de dados que propus na pergunta.
TheIronKnuckle 19/03/12
@TheIronKnuckle não muito diferença que as listas infinitas que Haskellers usar todo o tempo :)
Justin L.
37

É verdade que os gráficos não são algébricos. Para lidar com esse problema, você tem algumas opções:

  1. Em vez de gráficos, considere árvores infinitas. Representa ciclos no gráfico como seus desdobramentos infinitos. Em alguns casos, você pode usar o truque conhecido como "amarrar o nó" (explicado bem em algumas das outras respostas aqui) para representar essas árvores infinitas no espaço finito, criando um ciclo na pilha; no entanto, você não poderá observar ou detectar esses ciclos no Haskell, o que dificulta ou impossibilita várias operações gráficas.
  2. Há uma variedade de álgebras gráficas disponíveis na literatura. O que vem à mente primeiro é a coleção de construtores de gráficos descritos na seção dois de Transformações de gráficos bidirecionais . A propriedade usual garantida por essas álgebras é que qualquer gráfico pode ser representado algebricamente; no entanto, criticamente, muitos gráficos não terão uma representação canônica . Portanto, verificar a igualdade estruturalmente não é suficiente; fazê-lo corretamente se resume a encontrar isomorfismo gráfico - conhecido por ser um problema difícil.
  3. Desista de tipos de dados algébricos; representam explicitamente a identidade do nó, atribuindo a cada um deles valores exclusivos (digamos 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.
  4. Crie uma nova abordagem que se ajuste exatamente ao seu caso de uso. Isso é uma coisa muito difícil de fazer. =)

Portanto, existem prós e contras em cada uma das opções acima. Escolha o que lhe parecer melhor.

Daniel Wagner
fonte
"você não será capaz de observar ou detectar esses ciclos a partir do Haskell" não é exatamente verdade - há uma biblioteca que permite fazer exatamente isso! Veja minha resposta.
Artelius
gráficos são algébricos agora! hackage.haskell.org/package/algebraic-graphs
Josh.F
16

Alguns outros mencionaram brevemente fglos 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:

type Node = Int
type Adj b = [(b, Node)]
type Context a b = (Adj b, Node, a, Adj b)
data Graph a b = Empty | Context a b & Graph a b

(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 Nodetem um rótulo de algum tipo a; uma aresta tem um rótulo de algum tipo b. A Contexté 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ó. A Graphpode então ser concebido indutivamente como um Emptyou como um Context(com &) fundido em um existente Graph.

Como observa Erwig, não podemos gerar livremente um Graphcom Emptye &, como podemos gerar uma lista com os construtores Conse Nil, ou um Treecom Leafe Branch. Além disso, ao contrário das listas (como outros já mencionaram), não haverá nenhuma representação canônica de a Graph. 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 Graphtipo 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 de Contextcada 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.

liminalisht
fonte
14

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 .

Nicolas Trangez
fonte
3
Para expandir isso de maneira bastante aproximada, ele fornece um tipo de gráfico abstrato no qual você pode fazer a correspondência de padrões. O compromisso necessário para fazer esse trabalho é que a maneira exata de decompor um gráfico não é exclusiva, portanto o resultado de uma correspondência de padrão pode ser específico da implementação. Não é grande coisa na prática. Se você está curioso para saber mais sobre isso, escrevi um post introdutório do blog que pode ser uma leitura indescritível.
Tikhon Jelvis
Vou tomar uma liberdade e postar a boa conversa de Tikhon sobre este begriffs.com/posts/2015-09-04-pure-functional-graphs.html .
Martin Capodici 28/10
5

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:

-- Graph we want to represent:
--    .----> a <----.
--   /               \
--  b <------------.  \
--   \              \ / 
--    `----> c ----> d

-- Code for the graph:
a = leaf
b = node2 a c
c = node1 d
d = node2 a b
-- Yes, it's that simple!



-- If you want to convert the graph to a Node-Label format:
main = do
    g <- reifyGraph b   --can't use 'a' because not all nodes are reachable
    print g

Para executar o código acima, você precisará das seguintes definições:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
import Data.Reify
import Control.Applicative
import Data.Traversable

--Pointer-based graph representation
data PtrNode = PtrNode [PtrNode]

--Label-based graph representation
data LblNode lbl = LblNode [lbl] deriving Show

--Convenience functions for our DSL
leaf      = PtrNode []
node1 a   = PtrNode [a]
node2 a b = PtrNode [a, b]


-- This looks scary but we're just telling data-reify where the pointers are
-- in our graph representation so they can be turned to labels
instance MuRef PtrNode where
    type DeRef PtrNode = LblNode
    mapDeRef f (PtrNode as) = LblNode <$> (traverse f as)

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.

Artelius
fonte
2

Eu gosto dessa implementação de um gráfico tirado daqui

import Data.Maybe
import Data.Array

class Enum b => Graph a b | a -> b where
    vertices ::  a -> [b]
    edge :: a -> b -> b -> Maybe Double
    fromInt :: a -> Int -> b
pyCthon
fonte