Haskell에서 그래프를 어떻게 표현합니까?


125

대수 데이터 유형을 사용하여 하스켈에서 트리 또는 목록을 표현하는 것은 쉽습니다. 하지만 그래프를 활자체로 표현하려면 어떻게해야할까요? 포인터가 필요한 것 같습니다. 나는 당신이 다음과 같은 것을 가질 수 있다고 생각합니다.

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

그리고 그것은 실행 가능할 것입니다. 그러나 그것은 약간 분리 된 느낌입니다. 구조의 다른 노드 사이의 링크는 목록의 현재 이전 요소와 다음 요소 또는 트리의 노드의 부모와 자식 사이의 링크만큼 견고하게 느껴지지 않습니다. 정의한대로 그래프에서 대수적 조작을 수행하는 것은 태그 시스템을 통해 도입 된 간접 수준에 의해 다소 방해를받을 것이라는 예감이 있습니다.

내가이 질문을하게 만드는 것은 주로이 의심의 느낌과 우아함에 대한 인식 때문입니다. Haskell에서 그래프를 정의하는 더 나은 / 더 수학적으로 우아한 방법이 있습니까? 아니면 본질적으로 어렵거나 근본적인 무언가를 우연히 발견 했습니까? 재귀 적 데이터 구조는 훌륭하지만 이것은 다른 것 같습니다. 트리 및 목록이 자체 참조되는 방식과는 다른 의미의 자체 참조 데이터 구조입니다. 목록과 트리는 유형 수준에서 자체 참조와 비슷하지만 그래프는 값 수준에서 자체 참조입니다.

그래서 무슨 일이 일어나고 있습니까?


12
기능 그래프 알고리즘에 대한 Martin Erwig의 논문 인 web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01을 참조하십시오 . 이것 fgl으로부터 개발 된 패키지.
John L

99 개 하스켈 문제 페이지를 표시하는 문제 해결 문맥에서 사용 그래프의 예. 또한 다양한 표현에 대한 짧은 소개가 있습니다.
dopamane

답변:


47

또한 순수한 언어로주기를 사용하여 데이터 구조를 표현하는 것이 어색합니다. 정말 문제가되는 것은주기입니다. 값을 공유 할 수 있기 때문에 유형의 멤버 (목록 및 트리 포함)를 포함 할 수있는 모든 ADT는 실제로 DAG (Directed Acyclic Graph)입니다. 근본적인 문제는 값 A와 B가 있고 A에 B가 포함되고 B에 A가 포함 된 경우 다른 값이 존재하기 전에 둘 다 생성 될 수 없다는 것입니다. Haskell은 게으 르기 때문에 Tying the Knot 이라는 트릭을 사용 하여이 문제 를 해결할 수 있지만, 그것은 내 뇌를 아프게합니다 (아직 많이하지 않았기 때문입니다). 나는 지금까지 Haskell보다 Mercury에서 내 실질적인 프로그래밍을 더 많이 해왔고 Mercury는 엄격하기 때문에 매듭이 도움이되지 않습니다.

일반적으로 당신이 제안하는 것처럼 추가 간접적 인 지시에 의지하기 전에 내가 이것을 겪었을 때; 종종 ID에서 실제 요소로의 맵을 사용하고 요소가 다른 요소 대신 ID에 대한 참조를 포함하도록합니다. (명백한 비 효율성을 제외하고) 내가 싫어하는 가장 큰 점은 존재하지 않는 ID를 찾거나 동일한 ID를 둘 이상의 ID에 할당하려고 할 때 발생할 수있는 오류가 발생할 수 있다는 것입니다. 요소. 물론 이러한 오류가 발생하지 않도록 코드를 작성할 수 있으며, 이러한 오류가 발생할 수있는 유일한 위치 가 제한 되도록 추상화 뒤에 숨길 수도 있습니다. 그러나 여전히 잘못된 것이 하나 더 있습니다.

그러나 "Haskell 그래프"에 대한 빠른 Google을 통해 http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling으로 이동했습니다 . 읽어 볼만한 가치가있는 것 같습니다.


62

shang의 대답에서 게으름을 사용하여 그래프를 나타내는 방법을 볼 수 있습니다. 이러한 표현의 문제점은 변경하기가 매우 어렵다는 것입니다. 매듭을 짓는 트릭은 그래프를 한 번 작성하고 나중에는 절대 변경되지 않는 경우에만 유용합니다.

실제로, 나는 실제로 싶어해야 내 그래프 뭔가를, 내가 더 많은 보행자 표현을 사용합니다 :

  • 가장자리 목록
  • 인접 목록
  • 각 노드에 고유 한 레이블을 부여하고 포인터 대신 레이블을 사용하며 레이블에서 노드까지 유한 맵을 유지합니다.

그래프를 자주 변경하거나 편집하려는 경우 Huet의 지퍼를 기반으로 한 표현을 사용하는 것이 좋습니다. 이것은 제어 흐름 그래프를 위해 GHC에서 내부적으로 사용되는 표현입니다. 여기에서 읽을 수 있습니다.


2
매듭을 묶는 또 다른 문제는 실수로 매듭을 풀고 많은 공간을 낭비하기가 매우 쉽다는 것입니다.
hugomg

(적어도 현재로서는) Tuft의 웹 사이트에 문제가있는 것 같으며 현재이 링크 중 어느 것도 작동하지 않습니다. 이에 대한 대체 미러를 찾았 습니다. Huet의 Zipper를 기반으로 한 응용 제어 흐름 그래프 , Hoopl : 데이터 흐름 분석 및 변환을위한 모듈 식 재사용 가능한 라이브러리
gntskn

37

Ben이 언급했듯이 Haskell의 순환 데이터는 "매듭 묶기"라는 메커니즘으로 구성됩니다. 실제로는 letor 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
이 데이터 구조는 그래프를 설명 할 수 없다는 점도 언급해야합니다. 그것은 단지 그들의 전개를 설명합니다. (무한 유한 한 공간에서 unfoldings,하지만 여전히 ...)
Rotsor

1
와. 나는 모든 답을 자세히 조사 할 시간이 없었지만, 이런 게으른 평가를 이용하는 것은 얇은 얼음 위에서 스케이트를 타는 것처럼 들린다 고 말할 것입니다. 무한 재귀에 빠져드는 것이 얼마나 쉬울까요? 여전히 멋진 물건이며 질문에서 제안한 데이터 유형보다 훨씬 기분이 좋습니다.
TheIronKnuckle

Haskellers가 :) 모든 시간을 사용하는 무한리스트에 비해 너무 많이하지 차이 @TheIronKnuckle
저스틴 L.

37

사실, 그래프는 대수적이지 않습니다. 이 문제를 처리하기 위해 몇 가지 옵션이 있습니다.

  1. 그래프 대신 무한 트리를 고려하십시오. 무한 전개로 그래프의주기를 나타냅니다. 어떤 경우에는 "매듭 묶기"(여기의 다른 답변 중 일부에서 잘 설명 됨)로 알려진 트릭을 사용하여 힙에 순환을 생성하여 유한 공간에서 이러한 무한 트리를 표현할 수도 있습니다. 그러나 Haskell 내에서 이러한주기를 관찰하거나 감지 할 수 없으므로 다양한 그래프 작업이 어렵거나 불가능합니다.
  2. 문헌에는 다양한 그래프 대수가 있습니다. 가장 먼저 떠오르는 것은 Bidirectionalizing Graph Transformations 섹션 2에 설명 된 그래프 생성자 모음입니다 . 이러한 대수에 의해 보장되는 일반적인 속성은 모든 그래프가 대수적으로 표현 될 수 있다는 것입니다. 그러나 비판적으로 많은 그래프에는 표준 표현 이 없습니다 . 따라서 구조적으로 평등을 확인하는 것만으로는 충분하지 않습니다. 이를 올바르게 수행하는 것은 어려운 문제로 알려진 그래프 동형을 찾는 것으로 귀결됩니다.
  3. 대수 데이터 유형을 포기하십시오. 각각의 고유 한 값 (예 : Ints)을 제공하고 대수적으로가 아닌 간접적으로 참조하여 노드 ID를 명시 적으로 나타냅니다 . 이것은 타입을 추상화하고 당신을 위해 간접적 인 인터페이스를 제공함으로써 훨씬 더 편리하게 만들 수 있습니다. 이것은 예를 들어 Hackage의 fgl 및 기타 실용적인 그래프 라이브러리에서 취한 접근 방식 입니다.
  4. 사용 사례에 정확히 맞는 새로운 접근 방식을 생각해보십시오. 이것은 매우 어려운 일입니다. =)

따라서 위의 각 선택에는 장단점이 있습니다. 당신에게 가장 잘 어울리는 것을 선택하십시오.


"Haskell 내에서 이러한주기를 관찰하거나 감지 할 수 없습니다"는 정확히 사실이 아닙니다. 그렇게 할 수있는 라이브러리가 있습니다! 내 대답을 참조하십시오.
Artelius

그래프는 이제 대수적입니다! hackage.haskell.org/package/algebraic-graphs
Josh.F

16

몇몇 다른 사람들이 간단히 언급 fgl하고 Martin Erwig의 Inductive Graphs and Functional Graph Algorithms를 언급 했지만, 실제로 유도 표현 접근 방식 뒤에있는 데이터 유형에 대한 감각을 제공하는 답변을 작성할 가치가있을 것입니다.

그의 논문에서 Erwig는 다음 유형을 제시합니다.

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은 약간 다르며 유형 클래스를 잘 사용하지만 아이디어는 본질적으로 동일합니다.)

Erwig는 노드와 간선에 레이블이 있고 모든 간선이 향하는 다중 그래프를 설명합니다. A Node에는 어떤 유형의 레이블이 있습니다 a. 모서리에는 특정 유형의 레이블이 있습니다 b. A Context는 단순히 (1) 레이블이 지정된 가장자리 목록입니다. 특정 노드 (2) 해당 노드 (3) 노드의 라벨 (4)을 가리키는 표지 에지들의리스트 로부터 노드. A는 Graph다음과 같은 하나의 유도 생각할 수있다 Empty, 또는로 Context(병합 &기존에) Graph.

Erwig가 지적했듯이 우리는 GraphEmpty하고 &우리가 가진 목록을 생성 할 수 있습니다로, Cons그리고 Nil생성자, 또는 Tree으로 LeafBranch. (다른 사람들이 언급했듯이) 목록과는 달리 Graph. 이것은 중요한 차이점입니다.

그럼에도 불구하고이 표현을 강력하게 만들고 목록과 트리의 전형적인 Haskell 표현과 비슷하게 만드는 것은 Graph여기 에서 데이터 유형이 귀납적으로 정의 된다는 것입니다 . 목록이 귀납적으로 정의된다는 사실은 우리가 그것에 대해 매우 간결하게 패턴 일치를 만들고, 단일 요소를 처리하고, 나머지 목록을 재귀 적으로 처리 할 수있게합니다. 마찬가지로 Erwig의 귀납적 표현을 사용하면 그래프를 한 번 Context에 하나씩 재귀 적으로 처리 할 수 ​​있습니다 . 이러한 그래프 표현은 그래프 ( gmap) 에 매핑하는 방법에 대한 간단한 정의 뿐만 아니라 그래프 ( )에 대해 정렬되지 않은 접기를 수행하는 방법에 적합합니다 ufold.

이 페이지의 다른 댓글은 훌륭합니다. 그러나 제가이 답변을 쓴 주된 이유는 "그래프는 대수적이지 않습니다."와 같은 문구를 읽을 때 어떤 독자들은 그래프를 표현하는 좋은 방법을 찾지 못했다는 (잘못된) 인상을 필연적으로 떠날 까봐 두렵기 때문입니다. Haskell에서 패턴 매칭, 매핑, 폴딩 또는 일반적으로 목록과 트리로 수행하는 데 익숙한 일종의 멋지고 기능적인 작업을 수행 할 수 있습니다.


14

나는 항상 여기에서 읽을 수있는 "Inductive Graphs and Functional Graph Algorithms"에서 Martin Erwig의 접근 방식을 좋아했습니다 . FWIW, 저는 한때 Scala 구현을 작성했습니다 . https://github.com/nicolast/scalagraphs를 참조 하십시오 .


3
이에 확장하기 매우 약, 그것은 당신에게 추상적 인 그래프 유형에 당신이 할 수있는 패턴 일치를 제공합니다. 이 작업을 수행하는 데 필요한 절충안은 그래프를 분해 할 수있는 정확한 방법이 고유하지 않으므로 패턴 일치의 결과가 구현별로 다를 수 있다는 것입니다. 실제로는 큰 문제가 아닙니다. 그것에 대해 더 배우고 싶으 시다면 , 나는 글을 읽을 수 있는 소개 블로그 포스트 를 썼습니다 .
티콘 Jelvis

나는 자유를 취하고이 begriffs.com/posts/2015-09-04-pure-functional-graphs.html에 Tikhon의 멋진 이야기를 게시 할 것 입니다.
Martin Capodici

5

Haskell에서 그래프를 표현하는 것에 대한 모든 논의에는 Andy Gill의 데이터 수정 라이브러리에 대한 언급이 필요합니다 (여기 에 논문이 있습니다 ).

"매듭 매듭"스타일 표현은 매우 우아한 DSL을 만드는 데 사용할 수 있습니다 (아래 예 참조). 그러나 데이터 구조는 제한적으로 사용됩니다. Gill의 라이브러리는 두 세계의 장점을 모두 제공합니다. "매듭 묶기"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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.