Oui, vous pouvez modéliser un graphique de type sécurisé, dirigé, éventuellement cyclique dans Dhall, comme ceci:
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" ]
Cette représentation garantit l'absence de bords cassés.
J'ai également transformé cette réponse en un package que vous pouvez utiliser:
Modifier: Voici des ressources pertinentes et des explications supplémentaires qui peuvent aider à éclairer ce qui se passe:
Commencez d'abord par le type Haskell suivant pour un arbre :
data Tree a = Node { id :: a, neighbors :: [ Tree a ] }
Vous pouvez considérer ce type comme une structure de données paresseuse et potentiellement infinie représentant ce que vous obtiendriez si vous continuiez à visiter des voisins.
Maintenant, supposons que la Tree
représentation ci-dessus est réellement la notre Graph
en renommant simplement le type de données en Graph
:
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... mais même si nous voulions utiliser ce type, nous n'avons pas de moyen de modéliser directement ce type dans Dhall parce que le langage Dhall ne fournit pas de support intégré pour les structures de données récursives. Alors que faisons-nous?
Heureusement, il existe en fait un moyen d'incorporer des structures de données récursives et des fonctions récursives dans un langage non récursif comme Dhall. En fait, il y a deux façons!
- Algèbres F - Utilisées pour implémenter la récursivité
- F-coalgebras - Utilisé pour implémenter la "corecursion"
La première chose que j'ai lue qui m'a présenté cette astuce a été le brouillon suivant de Wadler:
... mais je peux résumer l'idée de base en utilisant les deux types Haskell suivants:
{-# LANGUAGE RankNTypes #-}
-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)
... et:
{-# LANGUAGE ExistentialQuantification #-}
-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)
La façon dont cela LFix
et le GFix
travail sont que vous pouvez leur donner "une couche" de votre type récursif ou "corécursif" souhaité (c'est-à-dire le f
) et ils vous donnent alors quelque chose qui est aussi puissant que le type souhaité sans nécessiter le support de la langue pour la récursivité ou la corécursion .
Prenons l'exemple des listes. Nous pouvons modéliser "une couche" d'une liste en utilisant le ListF
type suivant :
-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next
Comparez cette définition à la façon dont nous définirions normalement une OrdinaryList
définition de type de données récursive ordinaire:
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
La principale différence est qu'il ListF
prend un paramètre de type supplémentaire ( next
), que nous utilisons comme espace réservé pour toutes les occurrences récursives / corécursives du type.
Maintenant, équipé de ListF
, nous pouvons définir des listes récursives et corécursives comme ceci:
type List a = LFix (ListF a)
type CoList a = GFix (ListF a)
... où:
List
est une liste récursive implémentée sans prise en charge linguistique pour la récursivité
CoList
est une liste corecursive implémentée sans prise en charge linguistique pour la corecursion
Ces deux types sont équivalents à ("isomorphic to") []
, ce qui signifie que:
- Vous pouvez convertir et inverser de façon réversible entre
List
et[]
- Vous pouvez convertir et inverser de façon réversible entre
CoList
et[]
Prouvons cela en définissant ces fonctions de conversion!
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
La première étape de la mise en œuvre du type Dhall a donc consisté à convertir le Graph
type récursif :
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... à la représentation co-récursive équivalente:
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)
... bien que pour simplifier un peu les types, je trouve qu'il est plus facile de se spécialiser GFix
dans le cas où f = GraphF
:
data GraphF a next = Node { id ::: a, neighbors :: [ next ] }
data Graph a = forall x . Graph x (x -> GraphF a x)
Haskell n'a pas d'enregistrements anonymes comme Dhall, mais si c'était le cas, nous pourrions simplifier davantage le type en insérant la définition de GraphF
:
data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })
Maintenant, cela commence à ressembler au type Dhall pour a Graph
, surtout si nous le remplaçons x
par node
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
Cependant, il y a encore une dernière partie délicate, qui est de savoir comment traduire ExistentialQuantification
Haskell en Dhall. Il s'avère que vous pouvez toujours traduire la quantification existentielle en quantification universelle (c'est-à-dire forall
) en utilisant l'équivalence suivante:
exists y . f y ≅ forall x . (forall y . f y -> x) -> x
Je crois que cela s'appelle la "skolémisation"
Pour plus de détails, voir:
... et cette dernière astuce vous donne le type Dhall:
let Graph
: Type
= forall (Graph : Type)
-> forall ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> Graph
... où forall (Graph : Type)
joue le même rôle que forall x
dans la formule précédente et forall (Node : Type)
joue le même rôle que forall y
dans la formule précédente.