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 Tree
representasi di atas adalah milik kita Graph
hanya 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 LFix
dan GFix
bekerja 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 ListF
jenis berikut :
-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next
Bandingkan definisi itu dengan bagaimana kita biasanya mendefinisikan OrdinaryList
definisi datatype rekursif biasa:
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
Perbedaan utama adalah yang ListF
mengambil 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
List
dan[]
- Anda dapat mengkonversi secara bolak-balik antara
CoList
dan[]
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 Graph
tipe 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 GFix
pada 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 x
dengan node
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
Namun, masih ada satu bagian rumit terakhir, yaitu bagaimana menerjemahkan ExistentialQuantification
dari 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 x
pada rumus sebelumnya dan forall (Node : Type)
memainkan peran yang sama seperti forall y
pada rumus sebelumnya.