Tak, możesz modelować wykres typu bezpieczny, ukierunkowany, być może cykliczny w Dhall, w następujący sposób:
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" ]
Ta reprezentacja gwarantuje brak złamanych krawędzi.
Zmieniłem również tę odpowiedź w pakiet, którego możesz użyć:
Edycja: Oto odpowiednie zasoby i dodatkowe wyjaśnienia, które mogą pomóc wyjaśnić, co się dzieje:
Najpierw zacznij od następującego typu Haskell dla drzewa :
data Tree a = Node { id :: a, neighbors :: [ Tree a ] }
Możesz myśleć o tym typie jako o leniwej i potencjalnie nieskończonej strukturze danych reprezentującej to, co byś otrzymał, gdybyś tylko odwiedzał sąsiadów.
Teraz udawajmy, że powyższa Tree
reprezentacja faktycznie jest nasza Graph
, zmieniając po prostu nazwę typu danych na Graph
:
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... ale nawet gdybyśmy chcieli użyć tego typu, nie mamy możliwości bezpośredniego modelowania tego typu w Dhall, ponieważ język Dhall nie zapewnia wbudowanej obsługi struktur danych rekurencyjnych. Więc co robimy?
Na szczęście istnieje sposób na osadzenie struktur danych rekurencyjnych i funkcji rekurencyjnych w nierekurencyjnym języku, takim jak Dhall. W rzeczywistości istnieją dwa sposoby!
Pierwszą rzeczą, którą przeczytałem, która zapoznała mnie z tą sztuczką, był następujący szkic posta Wadlera:
... ale mogę podsumować podstawowy pomysł, używając następujących dwóch typów Haskell:
{-# LANGUAGE RankNTypes #-}
-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)
... i:
{-# LANGUAGE ExistentialQuantification #-}
-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)
Sposób, że LFix
i GFix
praca jest, że można dać im „jedną warstwę” żądanej rekursywne lub „corecursive” typ (tj f
) i następnie daje coś, co jest tak potężny jak żądanego typu bez konieczności wsparcia językowego dla rekursji lub corecursion .
Użyjmy list jako przykładu. Możemy modelować „jedną warstwę” listy, używając następującego ListF
typu:
-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next
Porównaj tę definicję z tym, jak normalnie zdefiniowalibyśmy OrdinaryList
zwykłą rekurencyjną definicję typu danych:
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
Główną różnicą jest to, że ListF
pobiera jeden dodatkowy parametr type ( next
), którego używamy jako symbolu zastępczego dla wszystkich rekurencyjnych / corecursive wystąpień tego typu.
Teraz, wyposażeni w ListF
, możemy zdefiniować listy rekurencyjne i współbieżne w następujący sposób:
type List a = LFix (ListF a)
type CoList a = GFix (ListF a)
... gdzie:
List
to lista rekurencyjna zaimplementowana bez obsługi języka dla rekurencji
CoList
to lista Corecursive zaimplementowana bez obsługi języka dla Corecursion
Oba te typy są równoważne („izomorficzny z”) []
, co oznacza, że:
- Możesz konwertować odwracalnie tam iz powrotem pomiędzy
List
i[]
- Możesz konwertować odwracalnie tam iz powrotem pomiędzy
CoList
i[]
Udowodnijmy to, definiując funkcje konwersji!
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
Pierwszym krokiem w implementacji typu Dhall była konwersja Graph
typu rekurencyjnego :
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... do równoważnej reprezentacji ko-rekurencyjnej:
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)
... chociaż trochę upraszczając typy, łatwiej jest specjalizować GFix
się w przypadku, w którym f = GraphF
:
data GraphF a next = Node { id ::: a, neighbors :: [ next ] }
data Graph a = forall x . Graph x (x -> GraphF a x)
Haskell nie ma anonimowych rekordów takich jak Dhall, ale gdyby tak było, moglibyśmy jeszcze bardziej uprościć typ, wprowadzając definicję GraphF
:
data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })
Teraz zaczyna to wyglądać typu Dhall dotyczący Graph
zwłaszcza jeśli zastąpimy x
z node
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
Jednak jest jeszcze jedna trudna część, która polega na przetłumaczeniu ExistentialQuantification
Haskella na Dhall. Okazuje się, że zawsze można przełożyć kwantyfikację egzystencjalną na kwantyfikację uniwersalną (tj. forall
), Stosując następującą równoważność:
exists y . f y ≅ forall x . (forall y . f y -> x) -> x
Wierzę, że to się nazywa „skolemizacja”
Aby uzyskać więcej informacji, zobacz:
... a ta ostatnia sztuczka daje typ Dhalla:
let Graph
: Type
= forall (Graph : Type)
-> forall ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> Graph
... gdzie forall (Graph : Type)
odgrywa taką samą rolę jak forall x
w poprzedniej formule i forall (Node : Type)
odgrywa taką samą rolę jak forall y
w poprzedniej formule.