it-swarm.com.ru

Как вы представляете график в Haskell?

Достаточно просто представить дерево или список в haskell, используя алгебраические типы данных. Но как бы вы описали типографское представление графика? Кажется, вам нужны указатели. Я предполагаю, что вы могли бы иметь что-то вроде

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

И это будет работать. Тем не менее, он чувствует себя немного отделенным; Связи между различными узлами в структуре на самом деле не "кажутся" такими же прочными, как связи между текущими предыдущими и следующими элементами в списке или родителями и потомками узла в дереве. У меня есть предчувствие, что алгебраические манипуляции с графом, как я определил, будут несколько затруднены уровнем косвенности, введенным через систему тегов.

Именно это чувство сомнения и восприятия неуравновешенности заставляет меня задать этот вопрос. Есть ли лучший/более математически элегантный способ определения графов в Haskell? Или я наткнулся на что-то сложное/фундаментальное? Рекурсивные структуры данных приятны, но, похоже, это нечто другое. Самостоятельная ссылочная структура данных в том смысле, в каком деревья и списки являются самостоятельными ссылочными. Это похоже на списки и деревья на уровне типа, которые ссылаются на себя сами, но графики на уровне значений являются самостоятельными ссылками.

Так что же на самом деле происходит?

114
TheIronKnuckle

Мне также неловко пытаться представлять структуры данных с циклами на чистом языке. Это циклы, которые действительно проблема; Поскольку значения могут быть общими, любой ADT, который может содержать член типа (включая списки и деревья), действительно является DAG (направленным ациклическим графом). Основная проблема заключается в том, что если у вас есть значения A и B, где A содержит B и B содержит A, то ни один из них не может быть создан до того, как существует другой. Поскольку Haskell ленив, вы можете использовать уловку, известную как Tying the Knot , чтобы обойти это, но это причиняет боль моему мозгу (потому что я еще не сделал большую часть этого). До сих пор я занимался более серьезным программированием на Mercury, чем на Haskell, и Mercury строг, поэтому завязывание узлов не помогает.

Обычно, когда я сталкивался с этим раньше, я просто прибегал к дополнительной косвенности, как вы предлагаете; часто используя карту от идентификаторов до фактических элементов, и имея элементы, содержащие ссылки на идентификаторы, а не на другие элементы. Главное, что мне не понравилось в этом (кроме очевидной неэффективности), это то, что он чувствовал себя более хрупким, представляя возможные ошибки при поиске несуществующего идентификатора или пытаясь назначить один и тот же идентификатор более чем одному элемент. Конечно, вы можете написать код, чтобы эти ошибки не возникали, и даже скрыть его за абстракциями, чтобы единственные места, где такие ошибки могли возникать ограничены. Но это еще одна вещь, чтобы ошибиться.

Тем не менее, быстрый поиск по "графу Хаскелла" привел меня к http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , который выглядит как стоящее чтение.

43
Ben

В ответе Шанга вы можете увидеть, как изобразить график с помощью лени. Проблема с этими представлениями заключается в том, что их очень трудно изменить. Уловка завязывания узлов полезна, только если вы собираетесь построить график один раз, а потом он никогда не изменится.

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

  • Краевой список
  • Список смежности
  • Присвойте уникальную метку каждому узлу, используйте метку вместо указателя и сохраняйте конечную карту от меток до узлов

Если вы собираетесь часто менять или редактировать график, я рекомендую использовать представление, основанное на молнии Huet. Это представление, используемое внутри GHC для графов потоков управления. Вы можете прочитать об этом здесь:

57
Norman Ramsey

Как упоминал Бен, циклические данные в Хаскеле строятся с помощью механизма, называемого "связывание узла". На практике это означает, что мы пишем взаимно рекурсивные объявления, используя предложения 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 для поиска соседних узлов.

34
shang

Это правда, графики не являются алгебраическими. Чтобы справиться с этой проблемой, у вас есть несколько вариантов:

  1. Вместо графиков рассмотрим бесконечные деревья. Представлять циклы в графе как их бесконечные разворачивания. В некоторых случаях вы можете использовать уловку, известную как "связывание узла" (хорошо объясненную в некоторых других ответах здесь), чтобы даже представить эти бесконечные деревья в конечном пространстве, создав цикл в куче; однако вы не сможете наблюдать или обнаруживать эти циклы в Haskell, что затрудняет или делает невозможным множество операций с графами.
  2. Существует множество графовых алгебр, доступных в литературе. Первое, что приходит на ум, это коллекция конструкторов графов, описанная во втором разделе Двунаправленные преобразования графов . Обычное свойство, гарантированное этими алгебрами, состоит в том, что любой граф может быть представлен алгебраически; однако, что важно, многие графы не будут иметь канонического представления. Поэтому структурной проверки равенства недостаточно; делать это правильно сводится к нахождению изоморфизма графа - известного как трудная проблема.
  3. Отказаться от алгебраических типов данных; явно представлять идентичность узла, присваивая им каждое уникальное значение (скажем, Ints) и ссылаясь на них косвенно, а не алгебраически. Это можно сделать значительно более удобным, сделав тип абстрактным и предоставив интерфейс, который будет манипулировать вами. Этот подход используется, например, fgl и другими практическими библиотеками графов в Hackage.
  4. Придумайте новый подход, который точно соответствует вашему варианту использования. Это очень сложно сделать. знак равно

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

32
Daniel Wagner

Несколько других кратко упомянули fgl и Martin Erwig's Индуктивные графы и алгоритмы функциональных графов , но, вероятно, стоит написать ответ, который фактически дает представление о типах данных, лежащих в основе подхода индуктивного представления.

В своей статье Эрвиг представляет следующие типы:

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; Edge имеет метку некоторого типа b. Context - это просто (1) список помеченных ребер, указывающих на определенный узел, (2) рассматриваемый узел, (3) метку узла и (4) список помеченных ребер, указывающих на узел. Затем Graph может быть индуктивно задуман как Empty или как Context, объединенный (с &) в существующее Graph.

Как отмечает Эрвиг, мы не можем свободно генерировать Graph с Empty и &, так как мы можем сгенерировать список с конструкторами Cons и Nil, или Tree с Leaf и Branch. Кроме того, в отличие от списков (как уже упоминали другие), не будет никакого канонического представления Graph. Это принципиальные различия.

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

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

14
liminalisht

Мне всегда нравился подход Мартина Эрвига в "Индуктивных графах и алгоритмах функциональных графов", который вы можете прочитать здесь . FWIW, однажды я также написал реализацию Scala, см. https://github.com/nicolast/scalagraphs .

14
Nicolas Trangez

Любое обсуждение представления графов в 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 были гораздо более сложными.

3
Artelius

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

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
pyCthon