Ja, Sie können in Dhall einen typsicheren, gerichteten, möglicherweise zyklischen Graphen wie folgt modellieren:
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" ]
Diese Darstellung garantiert das Fehlen von gebrochenen Kanten.
Ich habe diese Antwort auch in ein Paket umgewandelt, das Sie verwenden können:
Bearbeiten: Hier sind relevante Ressourcen und zusätzliche Erklärungen, die helfen können, die Vorgänge zu beleuchten:
Beginnen Sie zunächst mit dem folgenden Haskell-Typ für einen Baum :
data Tree a = Node { id :: a, neighbors :: [ Tree a ] }
Sie können sich diesen Typ als eine faule und möglicherweise unendliche Datenstruktur vorstellen, die darstellt, was Sie erhalten würden, wenn Sie nur weiterhin Nachbarn besuchen würden.
Nun lassen Sie uns so tun , dass die obige Tree
Darstellung tatsächlich ist unsere Graph
nur durch Umbenennung der Datentyp Graph
:
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... aber selbst wenn wir diesen Typ verwenden wollten, haben wir keine Möglichkeit, diesen Typ direkt in Dhall zu modellieren, da die Dhall-Sprache keine integrierte Unterstützung für rekursive Datenstrukturen bietet. Also, was machen wir?
Glücklicherweise gibt es tatsächlich eine Möglichkeit, rekursive Datenstrukturen und rekursive Funktionen in eine nicht rekursive Sprache wie Dhall einzubetten. Tatsächlich gibt es zwei Möglichkeiten!
- F-Algebren - Dient zum Implementieren der Rekursion
- F-Kohlegebren - Wird verwendet, um "Corecursion" zu implementieren.
Das erste, was ich las, das mich in diesen Trick einführte, war der folgende Entwurf eines Beitrags von Wadler:
... aber ich kann die Grundidee mit den folgenden zwei Haskell-Typen zusammenfassen:
{-# LANGUAGE RankNTypes #-}
-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)
... und:
{-# LANGUAGE ExistentialQuantification #-}
-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)
Die Art LFix
und Weise und die GFix
Arbeit besteht darin, dass Sie ihnen "eine Ebene" Ihres gewünschten rekursiven oder "corecursiven" Typs (dh den f
) geben können, und sie geben Ihnen dann etwas, das so leistungsfähig ist wie der gewünschte Typ, ohne dass Sprachunterstützung für Rekursion oder Corecursion erforderlich ist .
Verwenden wir Listen als Beispiel. Wir können "eine Ebene" einer Liste mit dem folgenden ListF
Typ modellieren :
-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next
Vergleichen Sie diese Definition mit der Definition OrdinaryList
einer gewöhnlichen rekursiven Datentypdefinition:
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
Der Hauptunterschied besteht darin, dass ListF
ein zusätzlicher Typparameter ( next
) verwendet wird, den wir als Platzhalter für alle rekursiven / corecursiven Vorkommen des Typs verwenden.
Ausgestattet mit ListF
können wir nun rekursive und kursive Listen wie folgt definieren:
type List a = LFix (ListF a)
type CoList a = GFix (ListF a)
... wo:
List
ist eine rekursive Liste, die ohne Sprachunterstützung für die Rekursion implementiert ist
CoList
ist eine CoreCursive-Liste, die ohne Sprachunterstützung für Corecursion implementiert wurde
Beide Typen sind äquivalent zu ("isomorph zu") []
, was bedeutet, dass:
- Sie können zwischen
List
und reversibel hin und her konvertieren[]
- Sie können zwischen
CoList
und reversibel hin und her konvertieren[]
Lassen Sie uns das beweisen, indem wir diese Konvertierungsfunktionen definieren!
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
Der erste Schritt bei der Implementierung des Dhall-Typs bestand also darin, den rekursiven Graph
Typ zu konvertieren :
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... zur äquivalenten co-rekursiven Darstellung:
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)
... obwohl ich es zur Vereinfachung der Typen ein wenig einfacher finde, mich GFix
auf den Fall zu spezialisieren, in dem f = GraphF
:
data GraphF a next = Node { id ::: a, neighbors :: [ next ] }
data Graph a = forall x . Graph x (x -> GraphF a x)
Haskell hat keine anonymen Datensätze wie Dhall, aber wenn dies der Fall wäre, könnten wir den Typ weiter vereinfachen, indem wir die Definition von GraphF
:
data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })
Jetzt fängt dies an, wie der Dhall-Typ für a auszusehen Graph
, besonders wenn wir ersetzen x
durch node
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
Es gibt jedoch noch einen letzten kniffligen Teil, nämlich die Übersetzung ExistentialQuantification
von Haskell nach Dhall. Es stellt sich heraus, dass Sie existenzielle Quantifizierung immer in universelle Quantifizierung (dh forall
) unter Verwendung der folgenden Äquivalenz übersetzen können:
exists y . f y ≅ forall x . (forall y . f y -> x) -> x
Ich glaube, das nennt man "Skolemisierung"
Weitere Einzelheiten finden Sie unter:
... und dieser letzte Trick gibt Ihnen den Dhall-Typ:
let Graph
: Type
= forall (Graph : Type)
-> forall ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> Graph
... wobei forall (Graph : Type)
die gleiche Rolle wie forall x
in der vorherigen Formel und forall (Node : Type)
die gleiche Rolle wie forall y
in der vorherigen Formel spielt.