Як ви представляєте графік в Haskell?


125

Досить просто представити дерево або список у haskell, використовуючи алгебраїчні типи даних. Але як би ти почав типографічно представляти графік? Здається, що вам потрібно мати покажчики. Я здогадуюсь, ти можеш мати щось подібне

type Nodetag = String
type Neighbours = [Nodetag]
data Node a = Node a Nodetag Neighbours

І це було б працездатно. Однак він відчуває себе трохи розв'язаним; Зв'язки між різними вузлами в структурі насправді не "відчувають себе" настільки міцними, як зв'язки між поточним попереднім та наступним елементами у списку, або батьками та дітьми вузла на дереві. У мене є думка, що виконання алгебраїчних маніпуляцій на графіку, як я його визначив, було б дещо перешкоджене рівнем непрямості, що вводиться через систему тегів.

Саме це почуття сумніву і сприйняття неелегантності змушує мене задати це питання. Чи є кращий / більш математично елегантний спосіб визначення графіків у Haskell? Або я натрапив на щось суто важке / фундаментальне? Рекурсивні структури даних є приємними, але це здається чимось іншим. Структура даних самореференції, що відрізняється від того, наскільки дерева та списки є самореференційними. Це як списки та дерева є самореференційними на рівні типу, але графіки є самореференційними на рівні значення.

Так що насправді відбувається?


12
Можливо, вас зацікавить праця Мартіна Ервіга про алгоритми функціональних графіків: web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01 . fglПакет розроблений з -за цього.
Джон Л

На сторінці 99 проблем Haskell показано кілька прикладів графіків, які використовуються в контексті вирішення проблем. Він також має короткий вступ про різні уявлення.
допаман

Відповіді:


47

Мені також незручно намагатися представляти структури даних циклами чистою мовою. Саме цикли - це справді проблема; тому що значеннями можна поділити будь-який ADT, який може містити член типу (включаючи списки та дерева) - це дійсно DAG (Directed Acyclic Graph). Фундаментальне питання полягає в тому, що якщо у вас є значення A і B, а A містить B і B, що містить A, то жодне не може бути створене до того, як існує інше. Оскільки Хаскелл лінивий, ви можете використовувати фокус, відомий як " Зав'язування вузла", щоб обійти це, але це моє мозок болить (бо я ще цього багато не зробив). Я зробив більше свого істотного програмування в Меркурії, ніж Хаскелл, і Меркурій суворий, тому зав'язування вузлів не допомагає.

Зазвичай, коли я стикався з цим раніше, ніж я тільки вдався до додаткової непрямості, як ви припускаєте; часто використовуючи карту від ідентифікаторів до фактичних елементів, а елементи містять посилання на ідентифікатори, а не на інші елементи. Головне, що мені не сподобалося робити це (окрім очевидної неефективності), це те, що він почувався більш крихким, вводячи можливі помилки пошуку ідентифікатора, який не існує, або намагаються призначити той самий ідентифікатор більше одному елемент. Ви можете написати код, щоб ці помилки, звичайно, не траплялися, і навіть приховати його за абстракціями, щоб обмежувати лише місця, де такі помилки можуть бути обмежені. Але все одно варто помилитися.

Однак швидкий google для "Haskell graph" привів мене до http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , який виглядає як варто прочитати.


62

У відповіді Шан ви можете побачити, як зобразити графік за допомогою лінь. Проблема цих уявлень полягає в тому, що їх дуже важко змінити. Трюк зав'язування вузлів корисний лише в тому випадку, якщо ви збираєтеся побудувати графік один раз, а потім він ніколи не змінюється.

На практиці, якщо я насправді хочу щось зробити зі своїм графіком, я використовую більше пішохідних уявлень:

  • Список країв
  • Список суміжності
  • Надайте унікальний ярлик кожному вузлу, використовуйте мітку замість вказівника та зберігайте кінцеву карту від міток до вузлів

Якщо ви збираєтеся часто змінювати або редагувати графік, рекомендую використовувати представлення на основі блискавки Huet. Це подання, яке використовується внутрішньо в GHC для графіків потоків управління. Про це ви можете прочитати тут:


2
Ще одна проблема з зав’язуванням вузла полягає в тому, що дуже легко його випадково розв’язати і витратити багато місця.
hugomg

З веб-сайтом Tuft щось здається не так (принаймні на даний момент), і жодне з цих посилань наразі не працює. Мені вдалося знайти кілька альтернативних дзеркал для них: Прикладний графік потоку управління на основі блискавки Хуета
gntskn

37

Як згадував Бен, циклічні дані в Хаскеллі побудовані за механізмом, який називається "зав'язування вузла". На практиці це означає, що ми пишемо взаємно рекурсивні декларації за допомогою letабо whereпунктів, що працює, оскільки взаємно рекурсивні частини ліниво оцінюються.

Ось приклад типу графіка:

import Data.Maybe (fromJust)

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

data Graph a = Graph [Node a]

Як бачите, ми використовуємо фактичні Nodeпосилання замість непрямості. Ось як реалізувати функцію, яка будує графік зі списку асоціацій міток.

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

Ми беремо список (nodeLabel, [adjacentLabel])пар і будуємо фактичні Nodeзначення за допомогою проміжного списку пошуку (який і робить фактичне зав'язування вузла). Хитрість полягає в тому, що nodeLookupList(який має тип [(a, Node a)]) побудований за допомогою mkNode, яке, в свою чергу, відноситься nodeLookupListдо знаходження сусідніх вузлів.


20
Слід також зазначити, що ця структура даних не в змозі описати графіки. Він лише описує їх розгортання. (нескінченне розгортання у кінцевому просторі, але все ж ...)
Ротсор

1
Ого. У мене не було часу детально вивчити всі відповіді, але скажу, що експлуатувати ледачу оцінку, як це, звучить так, ніби ви катаєтеся на тонкому льоду. Як легко було б скотитися в нескінченну рекурсію? Досі дивовижний матеріал і відчуває себе набагато краще, ніж тип даних, який я запропонував у питанні.
TheIronKnuckle

@TheIronKnuckle не надто велика різниця, ніж нескінченні списки, якими весь час користуються Haskellers :)
Джастін Л.

37

Це правда, графіки не є алгебраїчними. Щоб вирішити цю проблему, у вас є кілька варіантів:

  1. Замість графіків розгляньте нескінченні дерева. Представити на графіку цикли як їх нескінченне розгортання. У деяких випадках ви можете використовувати трюк, відомий як "зав'язування вузла" (це добре пояснено в деяких інших відповідях тут), щоб навіть представити ці нескінченні дерева у кінцевому просторі, створивши цикл у купі; однак, ви не зможете спостерігати або виявляти ці цикли зсередини Haskell, що робить різні операції з графіком складними або неможливими.
  2. У літературі є різноманітні графічні алгебри. Першим, що спадає на думку, є колекція конструкторів графіків, описана у другому розділі Бідерекціоналізація графічних перетворень . Звичайна властивість, гарантована цими алгебрами, полягає в тому, що будь-який графік може бути представлений алгебраїчно; однак, критично, багато графіків не матимуть канонічного зображення. Тому структурна перевірка рівності недостатня; правильно це зводиться до пошуку ізоморфізму графа - як відомо, це є якоюсь важкою проблемою.
  3. Відмовтеся від алгебраїчних типів даних; явно представляють ідентичність вузла, надаючи їм кожне унікальне значення (скажімо, Ints) і посилаючись на них опосередковано, а не алгебраїчно. Це можна зробити набагато зручніше, зробивши тип абстрактним і надавши інтерфейс, який примикає непряме для вас. Такий підхід застосовується, наприклад, Fgl та іншими практичними бібліотеками графіків щодо Hackage.
  4. Придумайте новий підхід, який точно відповідає вашому використанню. Це дуже важко зробити. =)

Отже, у кожного з вищезазначених варіантів є плюси і мінуси. Виберіть той, який здається вам найкращим.


"Ви не зможете спостерігати або виявляти ці цикли зсередини Haskell" - це не зовсім так - є бібліотека, яка дозволяє робити саме це! Дивіться мою відповідь.
Артелій

графіки зараз є алгебраїчними! hackage.haskell.org/package/algebraic-graphs
Josh.F

16

Кілька інших коротко згадали fglі алгоритми індуктивних графіків та функціональних графіків Мартіна Ервіга , але, напевно, варто написати відповідь, яка насправді дає розуміння типів даних, що стоять за індуктивним підходом представлення.

У своїй роботі Ервіг представляє такі типи:

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

(Представництво в fglдещо іншому, і він добре використовує типові класи - але ідея по суті однакова.)

Ервіг описує мультиграф, в якому вузли та ребра мають мітки та в яких усі ребра спрямовані. А Nodeмає етикетку якогось типу a; край має ярлик якогось типу b. A Context- це просто (1) список мічених країв, що вказують на певний вузол, (2) відповідний вузол, (3) мітка вузла та (4) список мічених країв, що вказують від вузла. GraphМоже бути задумана индуктивно або як Empty, або як Contextоб'єднані (з &) в існуючий Graph.

Як зазначає Ервіг, ми не можемо вільно генерувати Graphз Emptyта &, як ми можемо створити список із конструкторами Consта Nil, або Treeз Leafі та Branch. Занадто, на відміну від списків (як уже згадували інші), канонічне зображення а не буде Graph. Це вирішальні відмінності.

Тим не менш, те, що робить це представлення настільки потужним і настільки схожим на типові Хаскелл-представлення списків і дерев, полягає в тому, що Graphтип даних тут індуктивно визначений . Той факт, що список визначений індуктивно, це те, що дозволяє нам настільки коротко відповідати шаблону на ньому, обробляти один елемент і рекурсивно обробляти решту списку; однаково, індуктивне представлення Ервіга дозволяє нам рекурсивно обробляти графік по одному Context. Таке зображення графіку піддається простому визначенню способу відображення графіка ( gmap), а також способу виконання невпорядкованих складок над графіками ( ufold).

Інші коментарі на цій сторінці чудові. Основна причина, чому я написав цю відповідь, полягає в тому, що коли я читаю фрази, такі як "графіки не є алгебраїчними", я побоююся, що деякі читачі неминуче відійдуть із (помилковим) враженням, що ніхто не знайшов приємного способу представити графіки в Haskell таким чином, що дозволяє узгоджувати візерунок на них, відображати їх по карті, складати їх або взагалі робити такі класні, функціональні речі, які ми звикли робити зі списками та деревами.


14

Мені завжди подобався підхід Мартіна Ервіга в «Індуктивних графіках та алгоритмах функціональних графіків», який ви можете прочитати тут . FWIW, я колись також написав реалізацію Scala, див. Https://github.com/nicolast/scalagraphs .


3
Щоб розширити цю проблему дуже грубо, це дає вам абстрактний тип графіка, за яким ви можете скласти узгодження. Необхідний компроміс для здійснення цієї роботи полягає в тому, що точний спосіб розкладання графіка не є унікальним, тому результат відповідності шаблону може бути специфічним для реалізації. На практиці це не велика справа. Якщо вам цікаво дізнатись більше про це, я написав вступний пост у блозі, який може розлютити прочитане.
Тихон Єлвіс

Я візьму на себе сміливість і опублікую приємну розмову Тихона під цим begriffs.com/posts/2015-09-04-pure-functional-graphs.html .
Мартін Каподічі

5

Будь-яка дискусія щодо представлення графіків у Haskell потребує згадування бібліотеки перетворення даних Енді Гілла (ось документ ).

Представлення стилю "зав'язуючи вузол" можна використовувати для створення дуже елегантних DSL (див. Приклад нижче). Однак структура даних має обмежене використання. Бібліотека Гілла дозволяє вам найкраще з обох світів. Можна використовувати DSL "прив'язування вузла", але потім перетворити графік на основі вказівника в графік на основі мітки, щоб ви могли запускати на ньому свої алгоритми вибору.

Ось простий приклад:

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

Для запуску вищевказаного коду знадобляться наступні визначення:

{-# 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)

Хочу наголосити, що це спрощений DSL, але небо є межею! Я розробив дуже функціональний DSL, включаючи приємний синтаксис, подібний до дерева, для того, щоб вузол транслював початкове значення для деяких своїх дітей, і багато зручних функцій для побудови конкретних типів вузлів. Звичайно, набагато більше задіяно визначення даних типу Node та mapDeRef.


2

Мені подобається ця реалізація графіка, взятого звідси

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.