Jak przedstawić wykres w Haskell?

Łatwo jest reprezentować drzewo lub listę w haskell za pomocą algebraicznych typów danych. Ale jak ty byś to zrobił typograficznie reprezentując Wykres? Wydaje się, że musisz mieć wskazówki. Zgaduję, że możesz mieć coś w stylu

type Nodetag = String
type Neighbours = [Nodetag]
data Node a = Node a Nodetag Neighbours
I to byłoby wykonalne. Jednak wydaje się to nieco odsprzęgnięte; powiązania między różnymi węzłami w strukturze tak naprawdę nie "czują" się tak solidne, jak powiązania między aktualnymi poprzednimi i kolejnymi elementami na liście, lub rodzicami i dzieci węzła na drzewie. Mam przeczucie, że wykonywanie algebraicznych manipulacji na wykresie, jak zdefiniowałem, byłoby nieco utrudnione przez poziom indrection wprowadzony przez system znaczników. To przede wszystkim uczucie zwątpienia i postrzeganie nieelegancji powoduje, że zadaję to pytanie. Czy istnieje lepszy/bardziej matematycznie elegancki sposób definiowania Wykresów w Haskell? A może natknąłem się na coś z natury twardego/fundamentalnego? Rekurencyjne struktury danych są słodkie, ale to chyba coś innego. Samo-referencjalna struktura danych w innym znaczeniu niż drzewa i listy. To tak, jakby listy i drzewa były samooceną na poziomie typu, ale wykresy są samooceną na poziomie wartości. Więc o co tak naprawdę chodzi?
Author: TheIronKnuckle, 2012-03-16

8 answers

Wydaje mi się również kłopotliwe, aby próbować reprezentować struktury danych za pomocą cykli w czystym języku. To cykle są naprawdę problemem; ponieważ wartości mogą być dzielone każdy ADT, który może zawierać członka typu (w tym listy i drzewa) jest naprawdę dag (Directed Acyclic Graph). Podstawową kwestią jest to, że jeśli masz wartości A i B, z a zawierającym B i B zawierającym A, to żadna z nich nie może być utworzona przed powstaniem drugiej. Ponieważ Haskell jest leniwy, możesz użyć sztuczki znanej jako Wiązanie węzła , aby to obejść, ale to sprawia, że boli mnie mózg(ponieważ nie zrobiłem jeszcze wiele). Zrobiłem więcej z mojego merytorycznego programowania w Mercury niż Haskell do tej pory, a Mercury jest ścisły, więc wiązanie węzłów nie pomaga.

Zazwyczaj, gdy już spotkałem się z tym wcześniej, uciekałem się do dodatkowej indirection, jak sugerujesz; często za pomocą mapy od ID do rzeczywistych elementów, a elementy zawierają odniesienia do ID zamiast do innych elementów. Na najważniejsze, że nie podobało mi się to (poza oczywistą nieefektywnością), to to, że czułem się bardziej kruchy, wprowadzając możliwe błędy szukania identyfikatora, który nie istnieje lub próbując przypisać ten sam IDENTYFIKATOR do więcej niż jednego elementu. Możesz napisać kod, aby te błędy nie wystąpiły, oczywiście, a nawet ukryć go za abstrakcjami, aby jedyne miejsca, w których takie błędy mogłyby wystąpić, były ograniczone. Ale to jeszcze jedna rzecz, żeby się pomylić.

Jednak szybkie google dla "Haskell graph" doprowadził mnie do http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , który wygląda na wartą przeczytania.

 41
Author: Ben,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-16 05:42:47

W odpowiedzi Shanga można zobaczyć jak przedstawić Wykres używając lenistwa. Problem z tymi reprezentacjami polega na tym, że są one bardzo trudne do zmiany. Sztuczka z wiązaniem węzłów jest przydatna tylko wtedy, gdy masz zamiar zbudować wykres raz, a potem nigdy się nie zmienia.

W praktyce, jeśli rzeczywiście chcę zrobić coś z moim grafem, używam bardziej pieszych reprezentacji:

  • Lista krawędzi
  • Lista Pomocnicza
  • nadaj unikalną etykietę dla każdego węzła, użyj etykiety zamiast wskaźnika i zachowaj skończoną mapę od etykiet do węzłów

Jeśli zamierzasz często zmieniać lub edytować Wykres, polecam użycie reprezentacji opartej na suwaku Hueta. Jest to reprezentacja używana wewnętrznie w GHC dla wykresów przepływu sterowania. Możesz o tym przeczytać tutaj:

 55
Author: Norman Ramsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-16 14:57:06

Jak wspomniał Ben, cykliczne dane w Haskell są konstruowane przez mechanizm zwany "wiązaniem węzła". W praktyce oznacza to, że piszemy wzajemnie rekurencyjne deklaracje za pomocą klauzul let lub where, co działa, ponieważ części wzajemnie rekurencyjne są leniwie oceniane.

Oto przykładowy typ wykresu:

import Data.Maybe (fromJust)

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

data Graph a = Graph [Node a]

Jak widzisz, używamy rzeczywistych odniesień Node zamiast indrection. Oto jak zaimplementować funkcję, która konstruuje Wykres z listy etykiet Stowarzyszenia.

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

Pobieramy listę par (nodeLabel, [adjacentLabel]) i konstruujemy rzeczywiste wartości Node za pomocą pośredniej listy wyszukiwania (która wykonuje rzeczywiste wiązanie węzłów). Sztuczka polega na tym, że nodeLookupList (który ma typ [(a, Node a)]) jest skonstruowany za pomocą mkNode, który z kolei odnosi się z powrotem do nodeLookupList, aby znaleźć sąsiednie węzły.

 34
Author: shang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-16 06:29:48

To prawda, wykresy nie są algebraiczne. Aby poradzić sobie z tym problemem, masz kilka opcji:

  1. zamiast Wykresów rozważ nieskończone drzewa. Reprezentują cykle na wykresie jako ich nieskończone rozwinięcie. W niektórych przypadkach możesz użyć sztuczki znanej jako "Wiązanie węzła" (wyjaśnionej dobrze w niektórych innych odpowiedziach tutaj), aby nawet reprezentować te nieskończone drzewa w skończonej przestrzeni, tworząc cykl w stercie; jednak nie będziesz w stanie zaobserwować ani wykryć tych cykli z w Haskell, co sprawia, że różne operacje grafu trudne lub niemożliwe.
  2. W literaturze dostępne są różne algebry grafów. Pierwszy przychodzi mi na myśl zbiór konstruktorów grafów opisanych w sekcji drugiej dwukierunkowych przekształceń grafu . Typową właściwością gwarantowaną przez te algebry jest to, że każdy Graf może być reprezentowany algebraicznie; jednak krytycznie, wiele grafów nie będzie miało kanonicznej reprezentacji. Więc sprawdzanie równości strukturalnie nie wystarczy; zrobienie tego poprawnie sprowadza się do znalezienia izomorfizmu grafu-znanego z trudnego problemu.
  3. Zrezygnuj z algebraicznych typów danych; jawnie reprezentuj tożsamość węzła, dając im każdą unikalną wartość (powiedzmy, Int s) i odnosząc się do nich pośrednio, a nie algebraicznie. Może to być znacznie wygodniejsze, czyniąc Typ abstrakcyjny i zapewniając interfejs, który żongluje indrection dla Ciebie. Takie jest podejście zrobione np. przez fgl i inne praktyczne biblioteki grafów na temat Hackage ' u.
  4. wymyśl zupełnie nowe podejście, które dokładnie pasuje do twojego przypadku użycia. Jest to bardzo trudna rzecz do zrobienia. =)

Są więc plusy i minusy każdego z powyższych wyborów. Wybierz ten, który wydaje się najlepszy dla Ciebie.

 30
Author: Daniel Wagner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-16 18:57:09

Zawsze podobało mi się podejście Martina Erwiga w "grafach indukcyjnych i algorytmach grafów funkcyjnych" , które możecie przeczytać tutaj . FWIW, kiedyś też napisałem implementację Scali, Zobacz https://github.com/nicolast/scalagraphs .

 14
Author: Nicolas Trangez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-22 09:03:59

Kilku innych krótko wspomniało fgl i grafy indukcyjne Martina Erwiga i algorytmy grafów funkcyjnych , ale prawdopodobnie warto napisać odpowiedź, która faktycznie daje poczucie typów danych stojących za podejściem reprezentacji indukcyjnej.

W swojej pracy Erwig przedstawia następujące rodzaje:]}
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

(reprezentacja w fgl jest nieco inna i dobrze wykorzystuje typeklasy - ale idea jest zasadniczo taka sama.)

Erwig opisuje Multigraf, w którym węzły i krawędzie mają etykiety i w którym wszystkie krawędzie są skierowane. A Node ma etykietę pewnego typu a; krawędź ma etykietę pewnego typu b. A Context to po prostu (1) Lista oznakowanych krawędzi wskazujących do konkretnego węzła, (2) dany węzeł, (3) Etykieta węzła i (4) Lista oznakowanych krawędzi wskazujących od węzła. A Graph może być następnie pomyślane indukcyjnie jako Empty, lub jako Context połączone (z &) w istniejące Graph.

Jak zauważa Erwig, nie możemy swobodnie generować Graph za pomocą Empty i &, ponieważ możemy wygenerować listę za pomocą konstruktorów Cons i Nil, lub Tree za pomocą Leaf i Branch. Również, w przeciwieństwie do list (jak wspominali inni), nie będzie żadnej kanonicznej reprezentacji Graph. Są to zasadnicze różnice.

Niemniej jednak, to, co sprawia, że reprezentacja ta jest tak potężna i tak podobna do typowych reprezentacji Haskella list i drzew, to że typ danych Graph jest indukcyjnie zdefiniowany. Fakt, że lista jest indukcyjnie zdefiniowana, pozwala nam tak zwięźle dopasować wzór na niej, przetwarzać pojedynczy element i rekurencyjnie przetwarzać resztę listy; podobnie, indukcyjna reprezentacja Erwiga pozwala nam rekurencyjnie przetwarzać jeden wykres Context na raz. W przeciwieństwie do innych typów grafów, Graf może być używany do tworzenia wykresów, a także do tworzenia niestandardowych zagięć na wykresach. wykresy (ufold).

Inne komentarze na tej stronie są świetne. Głównym powodem, dla którego napisałem tę odpowiedź jest jednak to, że kiedy czytam zwroty takie jak "wykresy nie są algebraiczne", obawiam się, że niektórzy czytelnicy nieuchronnie odejdą z (błędnym) wrażeniem, że nikt nie znalazł miłego sposobu na reprezentowanie Wykresów w Haskell w sposób, który pozwala na dopasowanie wzorców na nich, mapowanie na nich, składanie ich lub ogólnie robi coś fajnego, funkcjonalnego, do czego jesteśmy przyzwyczajeni listy i Drzewa.

 13
Author: liminalisht,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-10 18:53:41

Każda dyskusja o reprezentowaniu Wykresów w Haskell wymaga wzmianki o bibliotece data-reify Andy ' ego Gilla (tutaj jest papier ).

Reprezentacja stylu "wiązania węzła" może być używana do tworzenia bardzo eleganckich DSL(patrz przykład poniżej). Struktura danych ma jednak ograniczone zastosowanie. Biblioteka Gilla pozwala na to, co najlepsze z obu światów. Możesz użyć "wiązania węzła" DSL, ale następnie przekonwertować wykres oparty na wskaźniku na wykres oparty na etykiecie, dzięki czemu możesz uruchomić swoje algorytmy wybór.

Oto prosty przykład:

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

Do uruchomienia powyższego kodu potrzebne będą następujące definicje:

{-# 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)
Chcę podkreślić, że jest to uproszczona DSL, ale niebo jest granicą! zaprojektowałem bardzo rozbudowaną DSL, w tym ładną składnię podobną do drzewa, aby węzeł nadawał wartość początkową do niektórych jego dzieci, oraz wiele wygodnych funkcji do konstruowania określonych typów węzłów. Oczywiście, typ danych węzła i definicje mapDeRef byli bardziej zaangażowani.
 3
Author: Artelius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-11 01:09:23

Podoba mi się Ta implementacja wykresu pobranego z tutaj

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
 2
Author: pyCthon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-12 18:11:17