Sì, puoi modellare un grafico sicuro, diretto, possibilmente ciclico in Dhall, in questo modo:
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" ]
Questa rappresentazione garantisce l'assenza di bordi spezzati.
Ho anche trasformato questa risposta in un pacchetto che puoi usare:
Modifica: ecco le risorse pertinenti e ulteriori spiegazioni che possono aiutare a illuminare ciò che sta succedendo:
Innanzitutto, inizia dal seguente tipo di Haskell per un albero :
data Tree a = Node { id :: a, neighbors :: [ Tree a ] }
Puoi pensare a questo tipo come una struttura di dati pigra e potenzialmente infinita che rappresenta ciò che otterresti se continuassi a visitare i vicini.
Ora, facciamo finta che la Tree
rappresentazione sopra sia effettivamente nostra Graph
semplicemente rinominando il tipo di dati in Graph
:
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... ma anche se volessimo usare questo tipo non abbiamo modo di modellare direttamente quel tipo in Dhall perché il linguaggio Dhall non fornisce supporto incorporato per strutture di dati ricorsive. Quindi cosa facciamo?
Fortunatamente, esiste effettivamente un modo per incorporare strutture di dati ricorsive e funzioni ricorsive in un linguaggio non ricorsivo come Dhall. In effetti, ci sono due modi!
- F-algebre - Utilizzato per implementare la ricorsione
- F-coalgebras - Utilizzato per implementare la "corecursion"
La prima cosa che ho letto che mi ha fatto conoscere questo trucco è stata la seguente bozza di post di Wadler:
... ma posso riassumere l'idea di base usando i seguenti due tipi di Haskell:
{-# LANGUAGE RankNTypes #-}
-- LFix is short for "Least fixed point"
newtype LFix f = LFix (forall x . (f x -> x) -> x)
... e:
{-# LANGUAGE ExistentialQuantification #-}
-- GFix is short for "Greatest fixed point"
data GFix f = forall x . GFix x (x -> f x)
Il modo in cui LFix
e GFix
lavoro è che si può dare loro "un livello" del tuo ricorsivo desiderato o di tipo "corecursive" (cioè la f
) e poi dare qualcosa che è potente come il tipo desiderato senza la necessità di supporto linguistico per la ricorsione o corecursion .
Usiamo le liste come esempio. Possiamo modellare "un livello" di un elenco usando il seguente ListF
tipo:
-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next
Confronta quella definizione con il modo in cui normalmente definiremmo OrdinaryList
usando una normale definizione di tipo di dati ricorsivo:
data OrdinaryList a = Nil | Cons a (OrdinaryList a)
La differenza principale è che ListF
accetta un parametro di tipo aggiuntivo ( next
), che utilizziamo come segnaposto per tutte le occorrenze ricorsive / corecursive del tipo.
Ora, dotato di ListF
, possiamo definire elenchi ricorsivi e correttivi come questo:
type List a = LFix (ListF a)
type CoList a = GFix (ListF a)
... dove:
List
è un elenco ricorsivo implementato senza supporto linguistico per la ricorsione
CoList
è un elenco corecursive implementato senza supporto linguistico per corecursion
Entrambi questi tipi sono equivalenti a ("isomorfo a") []
, nel senso che:
- È possibile convertire in modo reversibile avanti e indietro tra
List
e[]
- È possibile convertire in modo reversibile avanti e indietro tra
CoList
e[]
Dimostriamolo definendo quelle funzioni di conversione!
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
Quindi il primo passo nell'implementazione del tipo Dhall è stato quello di convertire il Graph
tipo ricorsivo :
data Graph a = Node { id :: a, neighbors :: [ Graph a ] }
... alla equivalente rappresentazione ricorsiva:
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)
... anche se per semplificare un po 'i tipi trovo che sia più facile specializzarsi GFix
nel caso in cui f = GraphF
:
data GraphF a next = Node { id ::: a, neighbors :: [ next ] }
data Graph a = forall x . Graph x (x -> GraphF a x)
Haskell non ha record anonimi come Dhall, ma se lo facesse allora potremmo semplificare ulteriormente il tipo incorporando la definizione di GraphF
:
data Graph a = forall x . MakeGraph x (x -> { id :: a, neighbors :: [ x ] })
Ora questo inizia ad apparire come il tipo Dhall per a Graph
, specialmente se sostituiamo x
con node
:
data Graph a = forall node . MakeGraph node (node -> { id :: a, neighbors :: [ node ] })
Tuttavia, c'è ancora un'ultima parte difficile, che è come tradurre il ExistentialQuantification
da Haskell a Dhall. Si scopre che è sempre possibile tradurre la quantificazione esistenziale in quantificazione universale (cioè forall
) usando la seguente equivalenza:
exists y . f y ≅ forall x . (forall y . f y -> x) -> x
Credo che questo si chiama "skolemization"
Per maggiori dettagli, vedi:
... e quel trucco finale ti dà il tipo Dhall:
let Graph
: Type
= forall (Graph : Type)
-> forall ( MakeGraph
: forall (Node : Type)
-> Node
-> (Node -> { id : Text, neighbors : List Node })
-> Graph
)
-> Graph
... dove forall (Graph : Type)
gioca lo stesso ruolo forall x
della formula precedente e forall (Node : Type)
gioca lo stesso ruolo forall y
della formula precedente.