Kann der Typ der gültigen Graphen in Dhall codiert werden?

10

Ich möchte ein Wiki (eine Reihe von Dokumenten mit einem gerichteten Diagramm) in Dhall darstellen. Diese Dokumente werden in HTML gerendert, und ich möchte verhindern, dass jemals fehlerhafte Links generiert werden. Aus meiner Sicht könnte dies erreicht werden, indem entweder ungültige Diagramme (Diagramme mit Links zu nicht vorhandenen Knoten) durch das Typsystem nicht darstellbar gemacht werden oder indem eine Funktion geschrieben wird, um eine Liste von Fehlern in einem möglichen Diagramm zurückzugeben (z. B. "In möglichem Diagramm" X, Knoten A enthält einen Link zu einem nicht existierenden Knoten B ").

Eine naive Adjazenzlisten-Darstellung könnte ungefähr so ​​aussehen:

let Node : Type = {
    id: Text,
    neighbors: List Text
}
let Graph : Type = List Node
let example : Graph = [
    { id = "a", neighbors = ["b"] }
]
in example

Wie dieses Beispiel zeigt, lässt dieser Typ Werte zu, die keinen gültigen Graphen entsprechen (es gibt keinen Knoten mit der ID "b", aber der Knoten mit der ID "a" legt einen Nachbarn mit der ID "b" fest). Darüber hinaus ist es nicht möglich, eine Liste dieser Probleme durch Umklappen der Nachbarn jedes Knotens zu erstellen, da Dhall den String-Vergleich nicht beabsichtigt.

Gibt es eine Darstellung, die entweder die Berechnung einer Liste defekter Links oder den Ausschluss defekter Links durch das Typsystem ermöglicht?

UPDATE: Ich habe gerade entdeckt, dass Naturals in Dhall vergleichbar sind. Ich nehme an, eine Funktion könnte geschrieben werden, um ungültige Kanten ("defekte Links") und doppelte Verwendungen eines Bezeichners zu identifizieren, wenn die Bezeichner Naturals wären.

Die ursprüngliche Frage, ob ein Graphentyp definiert werden kann, bleibt jedoch offen.

Bjørn Westergard
quelle
Stellen Sie das Diagramm stattdessen als Liste von Kanten dar. Die Knoten können aus den vorhandenen Kanten abgeleitet werden. Jede Kante würde aus einem Quellknoten und einem Zielknoten bestehen, aber um getrennte Knoten aufzunehmen, kann das Ziel optional sein.
Chepner

Antworten:

18

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 TreeDarstellung tatsächlich ist unsere Graphnur 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 LFixund Weise und die GFixArbeit 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 ListFTyp modellieren :

-- `ListF` is short for "List functor"
data ListF a next = Nil | Cons a next

Vergleichen Sie diese Definition mit der Definition OrdinaryListeiner gewöhnlichen rekursiven Datentypdefinition:

data OrdinaryList a = Nil | Cons a (OrdinaryList a)

Der Hauptunterschied besteht darin, dass ListFein zusätzlicher Typparameter ( next) verwendet wird, den wir als Platzhalter für alle rekursiven / corecursiven Vorkommen des Typs verwenden.

Ausgestattet mit ListFkö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 Listund reversibel hin und her konvertieren[]
  • Sie können zwischen CoListund 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 GraphTyp 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 GFixauf 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 xdurch 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 ExistentialQuantificationvon 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 xin der vorherigen Formel und forall (Node : Type)die gleiche Rolle wie forall yin der vorherigen Formel spielt.

Gabriel Gonzalez
quelle
1
Vielen Dank für diese Antwort und für all die harte Arbeit, die erforderlich ist, um Dhall zu entwickeln! Könnten Sie vorschlagen, dass wesentliche Neulinge in Dhall / System F lesen könnten, um besser zu verstehen, was Sie hier getan haben und welche anderen möglichen Diagrammdarstellungen es geben könnte? Ich möchte in der Lage sein, das, was Sie hier getan haben, zu erweitern, um eine Funktion zu schreiben, die die Darstellung der Adjazenzliste aus einem beliebigen Wert Ihres Diagrammtyps durch eine Tiefensuche erstellen kann.
Bjørn Westergard
4
@ BjørnWestergard: Gern geschehen! Ich habe meine Antwort bearbeitet, um die Theorie dahinter zu erklären, einschließlich nützlicher Referenzen
Gabriel Gonzalez