Bisakah jenis grafik yang valid dikodekan dalam Dhall?

10

Saya ingin mewakili wiki (satu set dokumen yang terdiri dari grafik berarah) di Dhall. Dokumen-dokumen ini akan dirender ke HTML, dan saya ingin mencegah tautan rusak dari yang pernah dibuat. Seperti yang saya lihat, ini dapat dicapai baik dengan membuat grafik yang tidak valid (grafik dengan tautan ke node yang tidak ada) tidak dapat direpresentasikan melalui sistem tipe atau dengan menulis fungsi untuk mengembalikan daftar kesalahan dalam setiap grafik yang mungkin (misalnya "Dalam grafik yang mungkin X, Node A berisi tautan ke Node B yang tidak ada ").

Representasi daftar kedekatan yang naif mungkin terlihat seperti ini:

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

Seperti contoh ini membuat jelas, tipe ini mengakui nilai yang tidak sesuai dengan grafik yang valid (tidak ada simpul dengan id "b", tetapi simpul dengan id "a" menetapkan tetangga dengan id "b"). Selain itu, tidak mungkin untuk membuat daftar masalah ini dengan melipat tetangga dari setiap Node, karena Dhall tidak mendukung perbandingan string dengan desain.

Apakah ada representasi yang memungkinkan perhitungan daftar tautan terputus atau pengecualian tautan terputus melalui sistem tipe?

UPDATE: Saya baru saja menemukan bahwa Natur dapat dibandingkan di Dhall. Jadi saya kira suatu fungsi dapat ditulis untuk mengidentifikasi setiap tepi yang tidak valid ("tautan terputus") dan menggandakan penggunaan pengidentifikasi jika pengidentifikasi adalah Naturals.

Namun, pertanyaan awal, apakah jenis Grafik dapat ditentukan, tetap ada.

Bjørn Westergard
sumber
Tunjukkan grafik sebagai daftar tepi. Node dapat disimpulkan dari tepi yang ada. Setiap tepi akan terdiri dari node sumber dan node tujuan, tetapi untuk mengakomodasi node yang terputus, tujuan dapat opsional.
chepner

Jawaban:

18

Ya, Anda dapat memodelkan grafik tipe-aman, terarah, mungkin-siklik, di Dhall, seperti ini:

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

Representasi ini menjamin tidak adanya tepi yang patah.

Saya juga mengubah jawaban ini menjadi paket yang dapat Anda gunakan:

Sunting: Berikut adalah sumber daya yang relevan dan penjelasan tambahan yang dapat membantu menerangi apa yang sedang terjadi:

Pertama, mulai dari jenis Haskell berikut untuk pohon :

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

Anda dapat menganggap tipe ini sebagai struktur data yang malas dan berpotensi tak terbatas yang mewakili apa yang akan Anda dapatkan jika Anda terus mengunjungi tetangga.

Sekarang, mari kita berpura-pura bahwa Treerepresentasi di atas adalah milik kita Graphhanya dengan mengubah nama tipe data menjadi Graph:

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

... tetapi bahkan jika kita ingin menggunakan tipe ini, kita tidak memiliki cara untuk secara langsung memodelkan tipe itu di Dhall karena bahasa Dhall tidak menyediakan dukungan bawaan untuk struktur data rekursif. Jadi apa yang kita lakukan?

Untungnya, sebenarnya ada cara untuk menanamkan struktur data rekursif dan fungsi rekursif dalam bahasa non-rekursif seperti Dhall. Sebenarnya, ada dua cara!

  • F-algebras - Digunakan untuk menerapkan rekursi
  • F-coalgebras - Digunakan untuk mengimplementasikan "corecursion"

Hal pertama yang saya baca yang memperkenalkan saya pada trik ini adalah draft postingan berikut oleh Wadler:

... tapi saya bisa meringkas ide dasar menggunakan dua jenis Haskell berikut:

{-# LANGUAGE RankNTypes #-}

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

... dan:

{-# LANGUAGE ExistentialQuantification #-}

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

Cara itu LFixdan GFixbekerja adalah bahwa Anda dapat memberi mereka "satu lapisan" dari tipe rekursif atau "korekur" yang Anda inginkan (yaitu f) dan mereka kemudian memberi Anda sesuatu yang sekuat jenis yang diinginkan tanpa memerlukan dukungan bahasa untuk rekursi atau korosi. .

Mari kita gunakan daftar sebagai contoh. Kami dapat memodelkan "satu lapisan" dari daftar menggunakan ListFjenis berikut :

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

Bandingkan definisi itu dengan bagaimana kita biasanya mendefinisikan OrdinaryListdefinisi datatype rekursif biasa:

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

Perbedaan utama adalah yang ListFmengambil satu parameter tipe ekstra ( next), yang kami gunakan sebagai pengganti untuk semua kejadian rekursif / korosif jenis.

Sekarang, dilengkapi dengan ListF, kita dapat mendefinisikan daftar rekursif dan korosif seperti ini:

type List a = LFix (ListF a)

type CoList a = GFix (ListF a)

... dimana:

  • List adalah daftar rekursif yang diimplementasikan tanpa dukungan bahasa untuk rekursi
  • CoList adalah daftar korektif yang diterapkan tanpa dukungan bahasa untuk korosi

Kedua jenis ini setara dengan ("isomorfik dengan") [], yang berarti bahwa:

  • Anda dapat mengkonversi secara bolak-balik antara Listdan[]
  • Anda dapat mengkonversi secara bolak-balik antara CoListdan[]

Mari kita buktikan dengan mendefinisikan fungsi konversi tersebut!

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

Jadi langkah pertama dalam mengimplementasikan tipe Dhall adalah mengubah Graphtipe rekursif :

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

... dengan representasi co-rekursif yang setara:

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)

... walaupun untuk menyederhanakan jenisnya sedikit saya merasa lebih mudah untuk mengkhususkan GFixpada kasus di mana f = GraphF:

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

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

Haskell tidak memiliki catatan anonim seperti Dhall, tetapi jika itu dilakukan maka kita dapat menyederhanakan jenis lebih lanjut dengan menggariskan definisi GraphF:

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

Sekarang ini mulai terlihat seperti tipe Dhall untuk Graph, terutama jika kita ganti xdengan node:

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

Namun, masih ada satu bagian rumit terakhir, yaitu bagaimana menerjemahkan ExistentialQuantificationdari Haskell ke Dhall. Ternyata Anda selalu dapat menerjemahkan kuantifikasi eksistensial ke kuantifikasi universal (yaitu forall) menggunakan kesetaraan berikut:

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

Saya percaya ini disebut "skolemisasi"

Untuk detail lebih lanjut, lihat:

... dan trik terakhir memberi Anda jenis Dhall:

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

... di mana forall (Graph : Type)memainkan peran yang sama seperti forall xpada rumus sebelumnya dan forall (Node : Type)memainkan peran yang sama seperti forall ypada rumus sebelumnya.

Gabriel Gonzalez
sumber
1
Terima kasih banyak atas jawaban ini, dan untuk semua kerja keras yang diperlukan untuk mengembangkan Dhall! Bisakah Anda menyarankan pendatang baru materi ke Dhall / System F bisa membaca untuk lebih memahami apa yang telah Anda lakukan di sini, apa representasi grafik lain yang mungkin ada? Saya ingin dapat memperluas apa yang telah Anda lakukan di sini untuk menulis fungsi yang dapat menghasilkan representasi daftar adjacency dari nilai apa pun dari jenis Grafik Anda melalui pencarian pertama yang mendalam.
Bjørn Westergard
4
@ BjørnWestergard: Sama-sama! Saya mengedit jawaban saya untuk menjelaskan teori di baliknya, termasuk referensi yang berguna
Gabriel Gonzalez