Làm thế nào để bạn đại diện cho một biểu đồ trong Haskell?


125

Thật dễ dàng để biểu diễn một cây hoặc danh sách trong haskell bằng cách sử dụng các kiểu dữ liệu đại số. Nhưng làm thế nào bạn sẽ đi về đại diện cho biểu đồ? Có vẻ như bạn cần phải có con trỏ. Tôi đoán bạn có thể có một cái gì đó như

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

Và đó sẽ là khả thi. Tuy nhiên, nó cảm thấy một chút tách rời; Các liên kết giữa các nút khác nhau trong cấu trúc không thực sự "cảm thấy" vững chắc như các liên kết giữa các yếu tố trước và hiện tại trong danh sách, hoặc cha mẹ và con cái của một nút trong cây. Tôi có linh cảm rằng việc thực hiện các thao tác đại số trên biểu đồ như tôi đã xác định nó sẽ bị cản trở phần nào bởi mức độ gián tiếp được giới thiệu thông qua hệ thống thẻ.

Chủ yếu là cảm giác nghi ngờ và nhận thức về sự không phù hợp này khiến tôi phải đặt câu hỏi này. Có cách nào tốt hơn / thanh lịch hơn về mặt toán học trong việc xác định đồ thị trong Haskell không? Hay tôi đã vấp phải một cái gì đó vốn đã khó / cơ bản? Cấu trúc dữ liệu đệ quy là ngọt ngào, nhưng điều này dường như là một cái gì đó khác. Cấu trúc dữ liệu tự tham chiếu theo nghĩa khác với cách cây và danh sách tự tham chiếu. Giống như danh sách và cây tự tham chiếu ở cấp độ loại, nhưng biểu đồ là tự tham chiếu ở cấp giá trị.

Vậy chuyện gì đang thực sự xảy ra?


12
Bạn có thể quan tâm đến bài viết của Martin Erwig về các thuật toán đồ thị chức năng: web.engr.oregonstate.edu/~erwig/ con / abstracts.html # JFP01 . Các fglgói phát triển ra khỏi này.
John L

Trang 99 vấn đề Haskell hiển thị một số ví dụ về biểu đồ được sử dụng trong ngữ cảnh giải quyết vấn đề. Nó cũng có một đoạn giới thiệu ngắn về các đại diện khác nhau.
dopamane

Câu trả lời:


47

Tôi cũng thấy khó xử khi cố gắng biểu diễn các cấu trúc dữ liệu theo chu kỳ bằng một ngôn ngữ thuần túy. Đó là các chu kỳ thực sự là vấn đề; bởi vì các giá trị có thể được chia sẻ bất kỳ ADT nào có thể chứa một thành viên của loại (bao gồm cả danh sách và cây) thực sự là một DAG (Đồ thị theo chu kỳ có hướng). Vấn đề cơ bản là nếu bạn có các giá trị A và B, với A chứa B và B chứa A, thì không thể tạo ra trước khi cái kia tồn tại. Bởi vì Haskell lười biếng, bạn có thể sử dụng một mẹo gọi là Buộc thắt nút để khắc phục điều này, nhưng điều đó khiến não tôi bị tổn thương (vì tôi chưa làm được gì nhiều). Cho đến nay, tôi đã thực hiện nhiều chương trình quan trọng hơn về Sao Thủy so với Haskell và Mercury rất nghiêm ngặt nên việc thắt nút không giúp ích gì.

Thông thường khi tôi gặp phải vấn đề này trước khi tôi dùng đến biện pháp bổ sung, như bạn đang đề xuất; thường bằng cách sử dụng bản đồ từ id đến các phần tử thực tế và có các phần tử chứa tham chiếu đến id thay vì các phần tử khác. Điều chính tôi không thích làm điều đó (ngoài sự kém hiệu quả rõ ràng) là nó cảm thấy mong manh hơn, đưa ra các lỗi có thể khi tìm kiếm một id không tồn tại hoặc cố gắng gán cùng một id cho nhiều hơn một thành phần. Bạn có thể viết mã để các lỗi này sẽ không xảy ra, tất nhiên, và thậm chí ẩn đằng sau nó trừu tượng để những nơi duy nhất mà những lỗi như vậy có thể xảy ra được bao bọc. Nhưng đó vẫn là một điều nữa để nhận sai.

Tuy nhiên, một google nhanh cho "đồ thị Haskell" đã dẫn tôi đến http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , có vẻ như đáng đọc.


62

Trong câu trả lời của shang, bạn có thể thấy cách biểu diễn đồ thị bằng cách sử dụng sự lười biếng. Vấn đề với các đại diện này là chúng rất khó thay đổi. Thủ thuật thắt nút chỉ hữu ích nếu bạn định xây dựng biểu đồ một lần và sau đó nó không bao giờ thay đổi.

Trong thực tế, nếu tôi thực sự muốn làm gì đó với biểu đồ của mình, tôi sử dụng các biểu diễn cho người đi bộ nhiều hơn:

  • Danh sách cạnh
  • Danh sách điều chỉnh
  • Cung cấp một nhãn duy nhất cho mỗi nút, sử dụng nhãn thay vì con trỏ và giữ bản đồ hữu hạn từ nhãn đến các nút

Nếu bạn thường xuyên thay đổi hoặc chỉnh sửa biểu đồ, tôi khuyên bạn nên sử dụng biểu diễn dựa trên khóa kéo của Huet. Đây là biểu diễn được sử dụng nội bộ trong GHC cho các biểu đồ luồng điều khiển. Bạn có thể đọc nó ở đây:


2
Một vấn đề khác với việc thắt nút là rất dễ vô tình tháo nó ra và lãng phí rất nhiều không gian.
hugomg

Có gì đó không ổn với trang web của Tuft (ít nhất là tại thời điểm này) và hiện tại cả hai liên kết này đều không hoạt động. Tôi đã tìm được một số gương thay thế cho những thứ này: Biểu đồ dòng điều khiển ứng dụng dựa trên Zipper của Huet , Hoopl: Thư viện mô đun, có thể tái sử dụng để phân tích và chuyển đổi dữ liệu
gntskn

37

Như Ben đã đề cập, dữ liệu tuần hoàn trong Haskell được xây dựng theo cơ chế gọi là "buộc nút". Trong thực tế, điều đó có nghĩa là chúng ta viết các khai báo đệ quy lẫn nhau bằng cách sử dụng lethoặc wherecác mệnh đề, hoạt động vì các phần đệ quy lẫn nhau được đánh giá một cách lười biếng.

Dưới đây là một loại biểu đồ ví dụ:

import Data.Maybe (fromJust)

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

data Graph a = Graph [Node a]

Như bạn có thể thấy, chúng tôi sử dụng các Nodetài liệu tham khảo thực tế thay vì chỉ định. Dưới đây là cách triển khai chức năng xây dựng biểu đồ từ danh sách các hiệp hội nhãn.

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

Chúng tôi lấy một danh sách các (nodeLabel, [adjacentLabel])cặp và xây dựng các Nodegiá trị thực tế thông qua một danh sách tra cứu trung gian (thực hiện thao tác thắt nút thực tế). Thủ thuật là nodeLookupList(có loại [(a, Node a)]) được xây dựng bằng cách sử dụng mkNode, từ đó quay trở lại nodeLookupListđể tìm các nút liền kề.


20
Bạn cũng nên đề cập rằng cấu trúc dữ liệu này không thể mô tả biểu đồ. Nó chỉ mô tả sự mở ra của họ. (mở ra vô hạn trong không gian hữu hạn, nhưng vẫn ...)
Rotsor

1
Ồ Tôi đã không có thời gian để kiểm tra tất cả các câu trả lời một cách chi tiết, nhưng tôi sẽ nói rằng việc khai thác đánh giá lười biếng như thế này nghe có vẻ như bạn đang trượt trên băng mỏng. Làm thế nào dễ dàng để trượt vào đệ quy vô hạn? Vẫn là những thứ tuyệt vời và cảm thấy tốt hơn nhiều so với kiểu dữ liệu mà tôi đề xuất trong câu hỏi.
TheIronKnuckle

@TheIronKnuckle không có quá nhiều sự khác biệt so với các danh sách vô hạn mà Haskeller sử dụng mọi lúc :)
Justin L.

37

Đó là sự thật, đồ thị không phải là đại số. Để giải quyết vấn đề này, bạn có một vài lựa chọn:

  1. Thay vì đồ thị, hãy xem xét cây vô hạn. Biểu diễn các chu kỳ trong biểu đồ dưới dạng mở rộng vô hạn của chúng. Trong một số trường hợp, bạn có thể sử dụng thủ thuật được gọi là "buộc nút" (giải thích rõ trong một số câu trả lời khác ở đây) để thậm chí đại diện cho những cây vô hạn này trong không gian hữu hạn bằng cách tạo một chu kỳ trong đống; tuy nhiên, bạn sẽ không thể quan sát hoặc phát hiện các chu kỳ này từ bên trong Haskell, điều này làm cho một loạt các hoạt động đồ thị trở nên khó khăn hoặc không thể.
  2. Có một loạt các đại số đồ thị có sẵn trong tài liệu. Điều đầu tiên bạn nghĩ đến là tập hợp các hàm tạo đồ thị được mô tả trong phần hai của Biến đổi đồ thị hai chiều . Thuộc tính thông thường được đảm bảo bởi các đại số này là bất kỳ biểu đồ nào cũng có thể được biểu diễn theo đại số; tuy nhiên, quan trọng, nhiều biểu đồ sẽ không có biểu diễn chính tắc . Vì vậy, kiểm tra bình đẳng về cấu trúc là không đủ; làm điều đó một cách chính xác để tìm ra sự đồng hình đồ thị - được biết đến là một vấn đề khó khăn.
  3. Từ bỏ các kiểu dữ liệu đại số; thể hiện rõ ràng nhận dạng nút bằng cách cho chúng từng giá trị duy nhất (giả sử, Ints) và tham chiếu đến chúng một cách gián tiếp thay vì đại số. Điều này có thể được thực hiện thuận tiện hơn đáng kể bằng cách làm cho loại trừu tượng và cung cấp một giao diện tung hứng cho bạn. Đây là cách tiếp cận được thực hiện bởi, ví dụ, fgl và các thư viện đồ thị thực tế khác trên Hackage.
  4. Hãy đến với một cách tiếp cận hoàn toàn mới phù hợp với trường hợp sử dụng của bạn một cách chính xác. Đây là một điều rất khó để làm. =)

Vì vậy, có những ưu và nhược điểm cho mỗi lựa chọn ở trên. Chọn một trong đó có vẻ tốt nhất cho bạn.


"Bạn sẽ không thể quan sát hoặc phát hiện các chu kỳ này từ bên trong Haskell" không chính xác - có một thư viện cho phép bạn làm điều đó! Xem câu trả lời của tôi.
Artelius

đồ thị bây giờ là đại số! hackage.haskell.org/package/acheebraic-graphs
Josh.F

16

Một vài người khác đã đề cập ngắn gọn fglThuật toán đồ thị quy nạp và thuật toán đồ thị chức năng của Martin Erwig , nhưng có lẽ đáng để viết một câu trả lời thực sự mang lại cảm giác về các kiểu dữ liệu đằng sau phương pháp biểu diễn quy nạp.

Trong bài báo của mình, Erwig trình bày các loại sau:

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

(Cách thể hiện trong fglhơi khác nhau và sử dụng tốt các kiểu chữ - nhưng về cơ bản là giống nhau.)

Erwig đang mô tả một đa dạng trong đó các nút và cạnh có nhãn và trong đó tất cả các cạnh đều được định hướng. A Nodecó nhãn của một số loại a; một cạnh có nhãn của một số loại b. A Contextchỉ đơn giản là (1) danh sách các cạnh được gắn nhãn chỉ đến một nút cụ thể, (2) nút được đề cập, (3) nhãn của nút và (4) danh sách các cạnh được gắn nhãn chỉ từ nút. A Graphsau đó có thể được hình thành theo quy nạp Empty, hoặc là Contexthợp nhất (với &) thành một hiện có Graph.

Như Erwig lưu ý, chúng ta không thể tự do tạo một Graphvới Empty&, vì chúng ta có thể tạo một danh sách với các hàm tạo ConsNilhoặc Treevới LeafBranch. Ngoài ra, không giống như các danh sách (như những người khác đã đề cập), sẽ không có bất kỳ đại diện chính tắc nào của a Graph. Đây là những khác biệt quan trọng.

Tuy nhiên, điều làm cho biểu diễn này trở nên mạnh mẽ và tương tự như các biểu diễn Haskell điển hình của danh sách và cây, là Graphkiểu dữ liệu ở đây được xác định theo quy nạp . Thực tế là một danh sách được xác định theo quy nạp là những gì cho phép chúng ta mô hình rất ngắn gọn khớp với nó, xử lý một yếu tố duy nhất và xử lý đệ quy phần còn lại của danh sách; đồng thời, biểu diễn quy nạp của Erwig cho phép chúng ta xử lý đệ quy một đồ thị Contexttại một thời điểm. Biểu diễn này của biểu đồ cho chính nó một định nghĩa đơn giản về cách ánh xạ trên biểu đồ ( gmap), cũng như cách thực hiện các nếp gấp không theo thứ tự trên biểu đồ ( ufold).

Các ý kiến ​​khác trên trang này là tuyệt vời. Tuy nhiên, lý do chính khiến tôi viết câu trả lời này là vì khi tôi đọc các cụm từ như "đồ thị không phải là đại số", tôi sợ rằng một số độc giả chắc chắn sẽ đi theo ấn tượng (sai lầm) rằng không ai tìm thấy một cách hay để biểu thị đồ thị trong Haskell theo cách cho phép khớp mẫu trên chúng, ánh xạ lên chúng, gấp chúng hoặc nói chung là thực hiện các công cụ chức năng thú vị mà chúng ta thường làm với danh sách và cây.


14

Tôi luôn thích cách tiếp cận của Martin Erwig trong "Đồ thị quy nạp và thuật toán đồ thị chức năng", mà bạn có thể đọc ở đây . FWIW, tôi cũng đã từng viết một triển khai Scala, xem https://github.com/nicolast/scal Đoạns .


3
Để mở rộng về điều này rất đại khái, nó cung cấp cho bạn một loại biểu đồ trừu tượng mà bạn có thể tạo mẫu khớp. Sự thỏa hiệp cần thiết để thực hiện công việc này là cách chính xác mà một biểu đồ có thể được phân tách không phải là duy nhất, do đó, kết quả của một kết quả khớp mẫu có thể là cụ thể thực hiện. Nó không phải là một vấn đề lớn trong thực tế. Nếu bạn tò mò muốn tìm hiểu thêm về nó, tôi đã viết một bài đăng trên blog giới thiệu có thể là một bài đọc.
Tikhon Jelvis

Tôi sẽ có một sự tự do và đăng bài nói chuyện hay của Tikhon về việc này xin vui lòng.com / posts / 2015-09-04-pure-feftal-graphs.html .
Martin Capodici

5

Bất kỳ cuộc thảo luận nào về biểu diễn đồ thị trong Haskell đều cần đề cập đến thư viện thống nhất dữ liệu của Andy Gill (đây là bài báo ).

Biểu diễn kiểu "buộc-thắt nút" có thể được sử dụng để tạo DSL rất thanh lịch (xem ví dụ bên dưới). Tuy nhiên, cấu trúc dữ liệu được sử dụng hạn chế. Thư viện của Gill cho phép bạn tốt nhất của cả hai thế giới. Bạn có thể sử dụng DSL "buộc nút", nhưng sau đó chuyển đổi biểu đồ dựa trên con trỏ thành biểu đồ dựa trên nhãn để bạn có thể chạy các thuật toán lựa chọn của mình trên đó.

Đây là một ví dụ đơn giản:

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

Để chạy mã trên, bạn sẽ cần các định nghĩa sau:

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

Tôi muốn nhấn mạnh rằng đây là một DSL đơn giản, nhưng giới hạn của bầu trời! Tôi đã thiết kế một DSL rất đặc trưng, ​​bao gồm một cú pháp giống như cây để có một nút phát một giá trị ban đầu cho một số phần tử con của nó và nhiều hàm tiện lợi để xây dựng các loại nút cụ thể. Tất nhiên, kiểu dữ liệu Node và định nghĩa mapDeRef có liên quan nhiều hơn.


2

Tôi thích việc thực hiện một biểu đồ được lấy từ đây

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.