คุณแสดงกราฟใน Haskell ได้อย่างไร?


125

ง่ายพอที่จะแสดงต้นไม้หรือรายการใน haskell โดยใช้ประเภทข้อมูลเกี่ยวกับพีชคณิต แต่คุณจะใช้กราฟแทนกราฟได้อย่างไร? ดูเหมือนว่าคุณต้องมีพอยน์เตอร์ ฉันเดาว่าคุณอาจมีบางอย่างเช่น

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

และนั่นจะสามารถใช้งานได้ อย่างไรก็ตามมันรู้สึกแยกออกเล็กน้อย การเชื่อมโยงระหว่างโหนดต่างๆในโครงสร้างไม่ได้ "ให้ความรู้สึก" มั่นคงเท่ากับการเชื่อมโยงระหว่างองค์ประกอบก่อนหน้าและถัดไปในปัจจุบันในรายการหรือพ่อแม่และลูกของโหนดในทรี ฉันมีลางสังหรณ์ว่าการปรับเปลี่ยนพีชคณิตบนกราฟตามที่ฉันกำหนดไว้มันจะถูกขัดขวางอยู่บ้างจากระดับของทิศทางที่แนะนำผ่านระบบแท็ก

โดยหลักแล้วมันคือความรู้สึกสงสัยและการรับรู้ถึงความไม่สมดุลที่ทำให้ฉันถามคำถามนี้ มีวิธีการกำหนดกราฟใน Haskell ที่ดีกว่า / สวยงามกว่าทางคณิตศาสตร์หรือไม่? หรือฉันสะดุดกับบางสิ่งบางอย่างที่ยาก / พื้นฐานโดยเนื้อแท้? โครงสร้างข้อมูลแบบเรียกซ้ำนั้นดูดี แต่ดูเหมือนจะเป็นอย่างอื่น โครงสร้างข้อมูลอ้างอิงตนเองในความหมายที่แตกต่างกันว่าต้นไม้และรายการอ้างอิงตนเองอย่างไร เช่นเดียวกับรายการและต้นไม้เป็นข้อมูลอ้างอิงตัวเองในระดับประเภท แต่กราฟจะอ้างอิงตัวเองในระดับค่า

แล้วเกิดอะไรขึ้น?


12
คุณอาจจะสนใจในกระดาษมาร์ติน Erwig ในขั้นตอนวิธีการทำงานกราฟ: web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01 fglแพคเกจการพัฒนามาจากนี้
John L

99 Haskell ปัญหาหน้าแสดงตัวอย่างของกราฟบางอย่างที่ใช้ในบริบทของการแก้ปัญหา นอกจากนี้ยังมีคำแนะนำสั้น ๆ เกี่ยวกับการนำเสนอที่แตกต่างกัน
dopamane

คำตอบ:


47

ฉันยังรู้สึกอึดอัดที่พยายามแสดงโครงสร้างข้อมูลด้วยวงจรในภาษาที่บริสุทธิ์ มันเป็นวัฏจักรที่เป็นปัญหาจริงๆ เนื่องจากสามารถแชร์ค่า ADT ใด ๆ ที่สามารถมีสมาชิกของประเภท (รวมถึงรายการและต้นไม้) จึงเป็น DAG (Directed Acyclic Graph) ปัญหาพื้นฐานคือถ้าคุณมีค่า A และ B โดยที่ A มี B และ B ประกอบด้วย A จะไม่สามารถสร้างขึ้นก่อนค่าอื่น ๆ ได้ เนื่องจาก Haskell เป็นคนขี้เกียจคุณสามารถใช้กลอุบายที่เรียกว่าTying the Knotเพื่อหลีกเลี่ยงสิ่งนี้ได้ แต่นั่นทำให้สมองของฉันบาดเจ็บ (เพราะฉันยังไม่ได้ทำอะไรมาก) ฉันได้เขียนโปรแกรมที่สำคัญใน Mercury มากกว่า Haskell จนถึงตอนนี้และ Mercury นั้นเข้มงวดดังนั้นการผูกปมก็ไม่ได้ช่วยอะไร

โดยปกติแล้วเมื่อฉันเจอสิ่งนี้ก่อนหน้านี้ฉันเพิ่งใช้ทิศทางเพิ่มเติมตามที่คุณแนะนำ บ่อยครั้งโดยใช้แผนที่จากรหัสไปยังองค์ประกอบจริงและมีองค์ประกอบที่มีการอ้างอิงถึงรหัสแทนที่จะเป็นองค์ประกอบอื่น ๆ สิ่งสำคัญที่ฉันไม่ชอบเกี่ยวกับการทำเช่นนั้น (นอกเหนือจากความไม่มีประสิทธิภาพที่เห็นได้ชัด) คือมันรู้สึกเปราะบางมากขึ้นทำให้เกิดข้อผิดพลาดที่เป็นไปได้ในการค้นหา id ที่ไม่มีอยู่หรือพยายามกำหนด id เดียวกันให้มากกว่าหนึ่ง ธาตุ. คุณสามารถเขียนโค้ดเพื่อไม่ให้เกิดข้อผิดพลาดเหล่านี้ได้แน่นอนและซ่อนไว้ด้านหลัง abstractions เพื่อให้สถานที่เดียวที่อาจเกิดข้อผิดพลาดดังกล่าวมีขอบเขต แต่ก็ยังมีอีกสิ่งหนึ่งที่จะผิดพลาด

อย่างไรก็ตาม Google อย่างรวดเร็วสำหรับ "กราฟ Haskell" ทำให้ฉันไปที่http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handlingซึ่งดูเหมือนว่าเป็นการอ่านที่คุ้มค่า


62

ในคำตอบของ shang คุณสามารถดูวิธีแสดงกราฟโดยใช้ความเกียจคร้าน ปัญหาเกี่ยวกับการเป็นตัวแทนเหล่านี้คือการเปลี่ยนแปลงนั้นยากมาก เคล็ดลับการผูกปมมีประโยชน์ก็ต่อเมื่อคุณจะสร้างกราฟเพียงครั้งเดียวและหลังจากนั้นจะไม่มีการเปลี่ยนแปลง

ในทางปฏิบัติฉันควรจะทำอะไรบางอย่างกับกราฟของฉันจริง ๆฉันใช้การแทนคนเดินเท้ามากกว่า:

  • รายการขอบ
  • รายการ Adjacency
  • กำหนดป้ายกำกับที่ไม่ซ้ำกันให้กับแต่ละโหนดใช้ป้ายกำกับแทนตัวชี้และเก็บแผนที่ จำกัด จากป้ายกำกับไปยังโหนด

หากคุณกำลังจะเปลี่ยนหรือแก้ไขกราฟบ่อยๆขอแนะนำให้ใช้การแสดงโดยอ้างอิงจากซิปของ Huet นี่คือการแสดงที่ใช้ภายใน GHC สำหรับกราฟโฟลว์ควบคุม คุณสามารถอ่านได้ที่นี่:


2
ปัญหาอีกอย่างในการผูกปมก็คือการคลายปมโดยไม่ตั้งใจทำได้ง่ายมากและสิ้นเปลืองพื้นที่มาก
hugomg

ดูเหมือนว่าจะมีบางอย่างผิดปกติกับเว็บไซต์ของ Tuft (อย่างน้อยก็ในขณะนี้) และลิงก์เหล่านี้ไม่ทำงานในขณะนี้ ฉันได้จัดการเพื่อค้นหามิเรอร์ทางเลือกสำหรับสิ่งเหล่านี้: กราฟการควบคุมการใช้งานที่อิงจากซิปของ Huet , Hoopl: ไลบรารีแบบแยกส่วนที่ใช้ซ้ำได้สำหรับการวิเคราะห์และการแปลง
กระแสข้อมูล

37

ดังที่เบ็นกล่าวถึงข้อมูลวัฏจักรใน Haskell สร้างขึ้นโดยกลไกที่เรียกว่า "การผูกปม" ในทางปฏิบัติหมายความว่าเราเขียนคำประกาศแบบเรียกซ้ำร่วมกันโดยใช้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
คุณควรระบุด้วยว่าโครงสร้างข้อมูลนี้ไม่สามารถอธิบายกราฟได้ เพียงอธิบายการตีแผ่ของพวกเขา (การคลี่ไม่สิ้นสุดในพื้นที่ จำกัด แต่ก็ยัง ... )
Rotsor

1
ว้าว. ฉันไม่มีเวลาตรวจสอบคำตอบทั้งหมดโดยละเอียด แต่ฉันจะบอกว่าการใช้ประโยชน์จากการประเมินที่ขี้เกียจเช่นนี้ดูเหมือนว่าคุณกำลังเล่นสเก็ตน้ำแข็งอยู่ มันจะง่ายแค่ไหนที่จะทำให้เกิดการเรียกซ้ำที่ไม่มีที่สิ้นสุด? ยังคงเป็นสิ่งที่ยอดเยี่ยมและรู้สึกดีกว่าประเภทข้อมูลที่ฉันเสนอในคำถาม
TheIronKnuckle

@TheIronKnuckle ไม่แตกต่างกันมากเกินไปกว่ารายการที่ไม่มีที่สิ้นสุดที่ Haskellers ใช้ตลอดเวลา :)
Justin L.

37

เป็นเรื่องจริงกราฟไม่ใช่พีชคณิต ในการจัดการกับปัญหานี้คุณมีสองตัวเลือก:

  1. แทนที่จะใช้กราฟให้พิจารณาต้นไม้ที่ไม่มีที่สิ้นสุด แสดงรอบในกราฟว่าเป็นการแผ่ที่ไม่มีที่สิ้นสุด ในบางกรณีคุณอาจใช้กลอุบายที่เรียกว่า "การผูกปม" (อธิบายได้ดีในคำตอบอื่น ๆ ที่นี่) เพื่อแสดงถึงต้นไม้ที่ไม่มีที่สิ้นสุดเหล่านี้ในพื้นที่ จำกัด โดยการสร้างวงจรในกอง อย่างไรก็ตามคุณจะไม่สามารถสังเกตหรือตรวจจับวัฏจักรเหล่านี้ได้จากภายใน Haskell ซึ่งทำให้การใช้งานกราฟต่างๆทำได้ยากหรือเป็นไปไม่ได้
  2. มีกราฟอัลเจบราที่มีอยู่มากมายในวรรณคดี หนึ่งที่อยู่ในใจครั้งแรกคือชุดของการก่อสร้างงานกราฟอธิบายไว้ในส่วนสองBidirectionalizing กราฟแปลง คุณสมบัติตามปกติที่รับประกันโดย algebras เหล่านี้คือกราฟใด ๆ ที่สามารถแสดงในเชิงพีชคณิตได้ อย่างไรก็ตามในเชิงวิกฤตกราฟจำนวนมากจะไม่มีการแสดงตามรูปแบบบัญญัติ ดังนั้นการตรวจสอบความเท่าเทียมกันในเชิงโครงสร้างจึงไม่เพียงพอ การทำอย่างถูกต้องจะทำให้เกิดการค้นหาไอโซมอร์ฟิซึมของกราฟซึ่งเป็นที่ทราบกันดีว่าเป็นปัญหาหนัก
  3. ยอมแพ้กับประเภทข้อมูลพีชคณิต; แสดงเอกลักษณ์ของโหนดอย่างชัดเจนโดยให้ค่าที่ไม่ซ้ำกัน (เช่นInts) และอ้างถึงโดยทางอ้อมมากกว่าเชิงพีชคณิต สิ่งนี้สามารถทำให้สะดวกมากขึ้นอย่างเห็นได้ชัดโดยการสร้างประเภทนามธรรมและจัดเตรียมอินเทอร์เฟซที่ปรับเปลี่ยนทิศทางให้กับคุณ นี่คือแนวทางที่ดำเนินการโดยเช่นfglและไลบรารีกราฟเชิงปฏิบัติอื่น ๆ บน Hackage
  4. คิดแนวทางใหม่ที่เหมาะกับกรณีการใช้งานของคุณอย่างแน่นอน นี่เป็นสิ่งที่ยากมากที่จะทำ =)

ดังนั้นจึงมีข้อดีและข้อเสียสำหรับแต่ละตัวเลือกข้างต้น เลือกสิ่งที่ดีที่สุดสำหรับคุณ


"คุณจะไม่สามารถสังเกตหรือตรวจจับวัฏจักรเหล่านี้ได้จากภายใน Haskell" นั้นไม่เป็นความจริง - มีห้องสมุดที่ให้คุณทำเช่นนั้นได้! ดูคำตอบของฉัน
Artelius

กราฟเป็นพีชคณิตตอนนี้! hackage.haskell.org/package/algebraic-graphs
Josh.F

17

มีอีกสองสามคนที่กล่าวถึงสั้น ๆfglและกราฟอุปนัยของ Martin Erwig และอัลกอริธึมกราฟเชิงฟังก์ชันแต่ก็น่าจะคุ้มค่าที่จะเขียนคำตอบที่ให้ความรู้สึกถึงประเภทข้อมูลที่อยู่เบื้องหลังแนวทางการแสดงอุปนัย

ในกระดาษของเขา 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 กำลังอธิบายถึงมัลติกราฟที่โหนดและขอบมีป้ายกำกับและมีการกำกับขอบทั้งหมด Nodeมีป้ายชื่อของบางประเภทa; bขอบมีป้ายชื่อของบางประเภท A Contextเป็นเพียง (1) รายการขอบป้ายที่ชี้ไปยังโหนดเฉพาะ (2) โหนดที่เป็นปัญหา (3) ป้ายชื่อของโหนดและ (4) รายการขอบที่มีป้ายกำกับที่ชี้จากโหนด Graphนั้นจะสามารถรู้สึกของ inductively เป็นอย่างใดอย่างหนึ่งEmptyหรือเป็นContextรวม (มี&) Graphลงในที่มีอยู่

เป็นบันทึก Erwig เราไม่สามารถได้อย่างอิสระสร้างGraphด้วยEmptyและ&ในขณะที่เราอาจสร้างรายการที่มีที่ConsและNilก่อสร้างหรือTreeด้วยและLeaf Branchเช่นกันซึ่งแตกต่างจากรายการ (ตามที่คนอื่น ๆ ได้กล่าวถึง) จะไม่มีการแทนมาตรฐานใด ๆ ของไฟล์Graph. นี่คือความแตกต่างที่สำคัญ

อย่างไรก็ตามสิ่งที่ทำให้การแสดงนี้เพื่อให้มีประสิทธิภาพและเพื่อให้คล้ายกับการแสดง Haskell โดยทั่วไปของรายการและต้นไม้เป็นที่Graphประเภทข้อมูลที่นี่จะกำหนด inductively ความจริงที่ว่ารายการถูกกำหนดโดยอุปนัยคือสิ่งที่ช่วยให้เราสามารถจับคู่รูปแบบที่กระชับประมวลผลองค์ประกอบเดียวและประมวลผลส่วนที่เหลือของรายการซ้ำได้ การแสดงแบบอุปนัยของ Erwig ช่วยให้เราสามารถประมวลผลกราฟซ้ำได้ทีContextละรายการ การแสดงกราฟนี้ให้คำจำกัดความง่ายๆของวิธีการแมปบนกราฟ ( gmap) รวมถึงวิธีการพับแบบไม่เรียงลำดับบนกราฟ ( ufold)

ความคิดเห็นอื่น ๆ ในหน้านี้ดีมาก อย่างไรก็ตามเหตุผลหลักที่ฉันเขียนคำตอบนี้ก็คือเมื่อฉันอ่านวลีเช่น "กราฟไม่ใช่พีชคณิต" ฉันกลัวว่าผู้อ่านบางคนจะได้รับความประทับใจ (ผิดพลาด) อย่างหลีกเลี่ยงไม่ได้ซึ่งไม่มีใครพบวิธีที่ดีในการแสดงกราฟ ใน Haskell ด้วยวิธีที่อนุญาตให้จับคู่รูปแบบกับพวกเขาทำแผนที่ทับพับหรือโดยทั่วไปทำสิ่งที่ยอดเยี่ยมและใช้งานได้ดีที่เราคุ้นเคยกับรายการและต้นไม้


14

ฉันมักจะชอบวิธีการที่มาร์ติน Erwig ใน "Inductive กราฟและฟังก์ชั่นกราฟอัลกอริทึม" ซึ่งคุณสามารถอ่านได้ที่นี่ FWIW ผมเคยเขียนการดำเนินงานที่สกาล่าเช่นกันดูhttps://github.com/nicolast/scalagraphs


3
การขยายตัวนี้มากประมาณว่ามันจะช่วยให้คุณกราฟประเภทนามธรรมที่คุณสามารถจับคู่รูปแบบ การประนีประนอมที่จำเป็นในการทำให้งานนี้คือวิธีที่แน่นอนของกราฟสามารถแยกย่อยได้นั้นไม่ซ้ำกันดังนั้นผลลัพธ์ของการจับคู่รูปแบบจึงสามารถเจาะจงการนำไปใช้งานได้ ไม่ใช่เรื่องใหญ่ในทางปฏิบัติ หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับเรื่องนี้ฉันได้เขียนบล็อกโพสต์เบื้องต้นซึ่งอาจทำให้อ่านไม่ได้
Tikhon Jelvis

ฉันจะใช้เสรีภาพและโพสต์พูดคุยที่ดีของทิคฮอน abouit นี้begriffs.com/posts/2015-09-04-pure-functional-graphs.html
Martin Capodici

5

การอภิปรายเกี่ยวกับการแสดงกราฟใน Haskell จำเป็นต้องมีการกล่าวถึงไลบรารี data-reifyของ 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 ที่มีคุณลักษณะมากรวมถึงไวยากรณ์แบบต้นไม้ที่ดีสำหรับการให้โหนดถ่ายทอดค่าเริ่มต้นไปยังลูก ๆ บางตัวและฟังก์ชันอำนวยความสะดวกมากมายสำหรับการสร้างประเภทโหนดเฉพาะ แน่นอนประเภทข้อมูลโหนดและคำจำกัดความ 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.