ชนิดของกราฟที่ถูกต้องสามารถเข้ารหัสใน Dhall ได้หรือไม่?


10

ฉันต้องการเป็นตัวแทนวิกิ (ชุดเอกสารที่ประกอบด้วยกราฟกำกับ) ใน Dhall เอกสารเหล่านี้จะแสดงผลเป็น HTML และฉันต้องการป้องกันไม่ให้มีลิงก์ที่เสียหายเกิดขึ้น เท่าที่ฉันเห็นมันสามารถทำได้โดยการสร้างกราฟที่ไม่ถูกต้อง (กราฟที่มีลิงก์ไปยังโหนดที่ไม่มีอยู่) ไม่สามารถแสดงผ่านระบบประเภทหรือโดยการเขียนฟังก์ชั่นเพื่อส่งกลับรายการข้อผิดพลาดในกราฟที่เป็นไปได้ X, Node A มีลิงค์ไปยัง Node B ที่ไม่มีอยู่จริง ")

การแทนค่ารายการ adjacency ที่ไร้เดียงสาอาจมีลักษณะเช่นนี้:

let Node : Type = {
    id: Text,
    neighbors: List Text
}
let Graph : Type = List Node
let example : Graph = [
    { id = "a", neighbors = ["b"] }
]
in example

เป็นตัวอย่างนี้ทำให้เห็นได้ชัดประเภทนี้ยอมรับค่าที่ไม่สอดคล้องกับกราฟที่ถูกต้อง (ไม่มีโหนดที่มี ID ของ "b" แต่โหนดที่มีรหัส "a" กำหนดเพื่อนบ้านที่มี id "b") ยิ่งไปกว่านั้นมันเป็นไปไม่ได้ที่จะสร้างรายการของปัญหาเหล่านี้โดยการพับที่เพื่อนบ้านของแต่ละโหนดเพราะ Dhall ไม่สนับสนุนการเปรียบเทียบสตริงโดยการออกแบบ

มีการเป็นตัวแทนที่จะช่วยให้การคำนวณรายชื่อของลิงค์ที่ขาดหรือการยกเว้นการเชื่อมโยงที่ขาดผ่านระบบประเภท?

UPDATE: ฉันเพิ่งค้นพบว่า Naturals เปรียบได้กับ Dhall ดังนั้นฉันคิดว่าฟังก์ชั่นสามารถเขียนเพื่อระบุขอบที่ไม่ถูกต้องใด ๆ ("ลิงก์ที่เสียหาย") และการใช้งานซ้ำของตัวระบุหากตัวระบุเป็น Naturals

แม้ว่าคำถามเดิมจะยังคงสามารถกำหนดชนิดของกราฟได้หรือไม่


แสดงกราฟเป็นรายการของขอบแทน โหนดสามารถอนุมานจากขอบที่มีอยู่ แต่ละขอบจะประกอบด้วยโหนดต้นทางและโหนดปลายทาง แต่เพื่อรองรับโหนดที่ไม่ได้เชื่อมต่อปลายทางสามารถเลือกได้
chepner

คำตอบ:


18

ใช่คุณสามารถสร้างรูปแบบกราฟปลอดภัยประเภทกำกับอาจเป็นวงจรใน Dhall เช่นนี้:

let List/map =
      https://prelude.dhall-lang.org/v14.0.0/List/map sha256:dd845ffb4568d40327f2a817eb42d1c6138b929ca758d50bc33112ef3c885680

let Graph
    : Type
    =     forall (Graph : Type)
      ->  forall  ( MakeGraph
                  :     forall (Node : Type)
                    ->  Node
                    ->  (Node -> { id : Text, neighbors : List Node })
                    ->  Graph
                  )
      ->  Graph

let MakeGraph
    :     forall (Node : Type)
      ->  Node
      ->  (Node -> { id : Text, neighbors : List Node })
      ->  Graph
    =     \(Node : Type)
      ->  \(current : Node)
      ->  \(step : Node -> { id : Text, neighbors : List Node })
      ->  \(Graph : Type)
      ->  \ ( MakeGraph
            :     forall (Node : Type)
              ->  Node
              ->  (Node -> { id : Text, neighbors : List Node })
              ->  Graph
            )
      ->  MakeGraph Node current step

let -- Get `Text` label for the current node of a Graph
    id
    : Graph -> Text
    =     \(graph : Graph)
      ->  graph
            Text
            (     \(Node : Type)
              ->  \(current : Node)
              ->  \(step : Node -> { id : Text, neighbors : List Node })
              ->  (step current).id
            )

let -- Get all neighbors of the current node
    neighbors
    : Graph -> List Graph
    =     \(graph : Graph)
      ->  graph
            (List Graph)
            (     \(Node : Type)
              ->  \(current : Node)
              ->  \(step : Node -> { id : Text, neighbors : List Node })
              ->  let neighborNodes
                      : List Node
                      = (step current).neighbors

                  let nodeToGraph
                      : Node -> Graph
                      =     \(node : Node)
                        ->  \(Graph : Type)
                        ->  \ ( MakeGraph
                              :     forall (Node : Type)
                                ->  forall (current : Node)
                                ->  forall  ( step
                                            :     Node
                                              ->  { id : Text
                                                  , neighbors : List Node
                                                  }
                                            )
                                ->  Graph
                              )
                        ->  MakeGraph Node node step

                  in  List/map Node Graph nodeToGraph neighborNodes
            )

let {- Example node type for a graph with three nodes

           For your Wiki, replace this with a type with one alternative per document
        -}
    Node =
      < Node0 | Node1 | Node2 >

let {- Example graph with the following nodes and edges between them:

                       Node0 ↔ Node1
                         ↓
                       Node2
                         ↺

           The starting node is Node0
        -}
    example
    : Graph
    = let step =
                \(node : Node)
            ->  merge
                  { Node0 = { id = "0", neighbors = [ Node.Node1, Node.Node2 ] }
                  , Node1 = { id = "1", neighbors = [ Node.Node0 ] }
                  , Node2 = { id = "2", neighbors = [ Node.Node2 ] }
                  }
                  node

      in  MakeGraph Node Node.Node0 step

in  assert : List/map Graph Text id (neighbors example) === [ "1", "2" ]

การเป็นตัวแทนนี้รับประกันว่าไม่มีขอบหัก

ฉันยังเปลี่ยนคำตอบนี้เป็นแพ็คเกจที่คุณสามารถใช้:

แก้ไข:นี่คือแหล่งข้อมูลที่เกี่ยวข้องและคำอธิบายเพิ่มเติมที่สามารถช่วยส่องสว่างสิ่งที่เกิดขึ้น:

ก่อนอื่นให้เริ่มจากประเภท Haskell ต่อไปนี้สำหรับทรี :

data Tree a = Node { id :: a, neighbors :: [ Tree a ] }

คุณสามารถคิดว่าประเภทนี้เป็นโครงสร้างข้อมูลที่ขี้เกียจและอาจไม่มีที่สิ้นสุดซึ่งแสดงสิ่งที่คุณจะได้รับหากคุณเพิ่งไปเยี่ยมเพื่อนบ้าน

ทีนี้สมมติว่าการTreeเป็นตัวแทนข้างต้นเป็นของเราGraphโดยเพียงแค่เปลี่ยนชื่อประเภทข้อมูลเป็นGraph:

data Graph a = Node { id :: a, neighbors :: [ Graph a ] }

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

โชคดีที่มีวิธีฝังโครงสร้างข้อมูลแบบเรียกซ้ำและฟังก์ชันแบบเรียกซ้ำในภาษาที่ไม่ใช่แบบเรียกซ้ำเช่น Dhall ในความเป็นจริงมีสองวิธี!

  • F-algebras - ใช้เพื่อดำเนินการเรียกซ้ำ
  • F-coalgebras - ใช้เพื่อดำเนินการ "corecursion"

สิ่งแรกที่ฉันอ่านที่แนะนำให้ฉันรู้จักกับเคล็ดลับนี้คือโพสต์ฉบับร่างโดย Wadler:

... แต่ฉันสามารถสรุปแนวคิดพื้นฐานโดยใช้ Haskell สองประเภทต่อไปนี้:

{-# LANGUAGE RankNTypes #-}

-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)

... และ:

{-# LANGUAGE ExistentialQuantification #-}

-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)

วิธีLFixและการGFixทำงานคือคุณสามารถให้ "ชั้นหนึ่ง" ของ recursive ที่คุณต้องการหรือ "corecursive" (เช่นf) และจากนั้นพวกเขาก็ให้บางอย่างที่ทรงพลังเท่ากับชนิดที่ต้องการโดยไม่ต้องการการสนับสนุนภาษาสำหรับการเรียกซ้ำหรือการตรวจสอบ .

ลองใช้รายการเป็นตัวอย่าง เราสามารถสร้างโมเดล "หนึ่งเลเยอร์" ของรายการโดยใช้ListFประเภทต่อไปนี้:

-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next

เปรียบเทียบคำจำกัดความนั้นกับวิธีที่เราปกติจะOrdinaryListใช้คำนิยามประเภทข้อมูลแบบเรียกซ้ำโดยทั่วไป:

data OrdinaryList a = Nil | Cons a (OrdinaryList a)

ความแตกต่างหลักคือListFใช้พารามิเตอร์ประเภทพิเศษหนึ่งรายการ ( next) ซึ่งเราใช้เป็นตัวยึดตำแหน่งสำหรับการเกิดซ้ำ / การกัดกร่อนทั้งหมดของประเภท

ตอนนี้มาพร้อมกับListFเราสามารถกำหนดรายการแบบเรียกซ้ำและแบบวนซ้ำดังนี้:

type List a = LFix (ListF a)

type CoList a = GFix (ListF a)

... ที่ไหน:

  • List เป็นรายการเรียกซ้ำที่ใช้งานโดยไม่สนับสนุนภาษาสำหรับการสอบถามซ้ำ
  • CoList เป็นรายการตรวจสอบความถูกต้องที่นำมาใช้โดยไม่สนับสนุนภาษาสำหรับการตรวจสอบข้อมูล

ทั้งสองประเภทนี้เทียบเท่ากับ ("isomorphic to") []ซึ่งหมายความว่า:

  • คุณสามารถแปลงกลับไปกลับมาระหว่างListและ[]
  • คุณสามารถแปลงกลับไปกลับมาระหว่างCoListและ[]

มาพิสูจน์กันโดยกำหนดฟังก์ชั่นการแปลงเหล่านั้น!

fromList :: List a -> [a]
fromList (LFix f) = f adapt
  where
    adapt (Cons a next) = a : next
    adapt  Nil          = []

toList :: [a] -> List a
toList xs = LFix (\k -> foldr (\a x -> k (Cons a x)) (k Nil) xs)

fromCoList :: CoList a -> [a]
fromCoList (GFix start step) = loop start
  where
    loop state = case step state of
        Nil           -> []
        Cons a state' -> a : loop state'

toCoList :: [a] -> CoList a
toCoList xs = GFix xs step
  where
    step      []  = Nil
    step (y : ys) = Cons y ys

ดังนั้นขั้นตอนแรกในการนำ Dhall ไปใช้คือการแปลงGraphชนิดเรียกซ้ำ:

data Graph a = Node { id :: a, neighbors :: [ Graph a ] }

... ถึงการนำเสนอร่วมแบบเรียกซ้ำ:

data GraphF a next = Node { id ::: a, neighbors :: [ next ] }

data GFix f = forall x . GFix x (x -> f x)

type Graph a = GFix (GraphF a)

... แม้ว่าจะทำให้ประเภทง่ายขึ้นเล็กน้อยฉันพบว่าง่ายกว่าGFixในกรณีที่f = GraphF:

data GraphF a next = Node { id ::: a, neighbors :: [ next ] }

data Graph a = forall x . Graph x (x -> GraphF a x)

Haskell ไม่มีบันทึกที่ไม่ระบุชื่อเช่น Dhall แต่ถ้าเป็นเช่นนั้นเราสามารถทำให้ประเภทนั้นง่ายขึ้นโดยการนิยามคำจำกัดความของGraphF:

data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })

ตอนนี้สิ่งนี้จะเริ่มดูเหมือน Dhall สำหรับ a Graphโดยเฉพาะถ้าเราแทนที่xด้วยnode:

data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })

อย่างไรก็ตามยังมีอีกหนึ่งส่วนสุดท้ายที่ยุ่งยากซึ่งเป็นวิธีการแปลExistentialQuantificationจาก Haskell เป็น Dhall ปรากฎว่าคุณสามารถแปลปริมาณการดำรงอยู่เป็นปริมาณสากล (เช่นforall) โดยใช้การเทียบเท่าดังต่อไปนี้:

exists y . f y ≅ forall x . (forall y . f y -> x) -> x

ฉันเชื่อว่าสิ่งนี้เรียกว่า "skolemization"

สำหรับรายละเอียดเพิ่มเติมดู:

... และเคล็ดลับสุดท้ายนั้นให้ประเภท Dhall:

let Graph
    : Type
    =     forall (Graph : Type)
      ->  forall  ( MakeGraph
                  :     forall (Node : Type)
                    ->  Node
                    ->  (Node -> { id : Text, neighbors : List Node })
                    ->  Graph
                  )
      ->  Graph

... โดยที่forall (Graph : Type)มีบทบาทเช่นเดียวกับforall xในสูตรก่อนหน้าและforall (Node : Type)มีบทบาทเช่นเดียวกับforall yในสูตรก่อนหน้า


1
ขอบคุณมากสำหรับคำตอบนี้และสำหรับการทุ่มเทอย่างหนักเพื่อพัฒนา Dhall! คุณช่วยแนะนำผู้ใช้วัสดุใหม่ให้กับ Dhall / System F สามารถอ่านเพื่อทำความเข้าใจกับสิ่งที่คุณทำที่นี่ได้ดียิ่งขึ้น ฉันต้องการที่จะขยายสิ่งที่คุณทำที่นี่เพื่อเขียนฟังก์ชั่นที่สามารถสร้างการแสดงรายการ adjacency จากค่าใด ๆ ของประเภทกราฟของคุณผ่านการค้นหาครั้งแรก
Bjørn Westergard

4
@ BjørnWestergard: ไม่เป็นไร! ฉันแก้ไขคำตอบของฉันเพื่ออธิบายทฤษฎีที่อยู่เบื้องหลังรวมถึงการอ้างอิงที่มีประโยชน์
Gabriel Gonzalez
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.